Commit e8b10924 by Tim Krones

Make DnDv2 accessible.

parent c90e8579
...@@ -153,6 +153,7 @@ class DragAndDropBlock(XBlock): ...@@ -153,6 +153,7 @@ class DragAndDropBlock(XBlock):
"question_text": self.question_text, "question_text": self.question_text,
"show_question_header": self.show_question_header, "show_question_header": self.show_question_header,
"target_img_expanded_url": self.target_img_expanded_url, "target_img_expanded_url": self.target_img_expanded_url,
"target_img_description": self.target_img_description,
"item_background_color": self.item_background_color or None, "item_background_color": self.item_background_color or None,
"item_text_color": self.item_text_color or None, "item_text_color": self.item_text_color or None,
"initial_feedback": self.data['feedback']['start'], "initial_feedback": self.data['feedback']['start'],
...@@ -197,6 +198,7 @@ class DragAndDropBlock(XBlock): ...@@ -197,6 +198,7 @@ class DragAndDropBlock(XBlock):
fragment.initialize_js('DragAndDropEditBlock', { fragment.initialize_js('DragAndDropEditBlock', {
'data': self.data, 'data': self.data,
'target_img_expanded_url': self.target_img_expanded_url, 'target_img_expanded_url': self.target_img_expanded_url,
'target_img_description': self.target_img_description,
'default_background_image_url': self.default_background_image_url, 'default_background_image_url': self.default_background_image_url,
}) })
...@@ -329,6 +331,11 @@ class DragAndDropBlock(XBlock): ...@@ -329,6 +331,11 @@ class DragAndDropBlock(XBlock):
return self.default_background_image_url return self.default_background_image_url
@property @property
def target_img_description(self):
""" Get the description for the target image (the image items are dragged onto). """
return self.data.get("targetImgDescription", "")
@property
def default_background_image_url(self): def default_background_image_url(self):
""" The URL to the default background image, shown when no custom background is used """ """ The URL to the default background image, shown when no custom background is used """
return self.runtime.local_resource_url(self, "public/img/triangle.png") return self.runtime.local_resource_url(self, "public/img/triangle.png")
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
/* Shared styles used in header and footer */ /* Shared styles used in header and footer */
.xblock--drag-and-drop .title1 { .xblock--drag-and-drop .title1 {
color: rgb(85, 85, 85); color: #555555;
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
align-items: center; align-items: center;
position: relative; position: relative;
border: 1px solid rgba(0,0,0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px; border-radius: 3px;
padding: 5px; padding: 5px;
} }
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
border-radius: 3px; border-radius: 3px;
margin: 5px; margin: 5px;
padding: 10px; padding: 10px;
background-color: #2e83cd; background-color: #2872b2;
font-size: 14px; font-size: 14px;
color: #fff; color: #fff;
opacity: 1; opacity: 1;
...@@ -112,8 +112,14 @@ ...@@ -112,8 +112,14 @@
transform: translate(-50%, -50%); /* These blocks are to be centered on their absolute x,y position */ transform: translate(-50%, -50%); /* These blocks are to be centered on their absolute x,y position */
} }
/* Focused option */
.xblock--drag-and-drop .drag-container .item-bank .option:focus,
.xblock--drag-and-drop .drag-container .item-bank .option:hover {
transform: scale(1.1);
}
.xblock--drag-and-drop .drag-container .ui-draggable-dragging { .xblock--drag-and-drop .drag-container .ui-draggable-dragging {
box-shadow: 0 16px 32px 0 rgba(0,0,0,.3); box-shadow: 0 16px 32px 0 rgba(0, 0, 0, 0.3);
border: 1px solid #ccc; border: 1px solid #ccc;
opacity: .65; opacity: .65;
z-index: 20 !important; z-index: 20 !important;
...@@ -159,7 +165,7 @@ ...@@ -159,7 +165,7 @@
.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input { .xblock--drag-and-drop .drag-container .option .numerical-input.correct .input {
background: #ceffce; background: #ceffce;
color: #0dad0d; color: #087108;
} }
.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input { .xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input {
...@@ -224,8 +230,12 @@ ...@@ -224,8 +230,12 @@
} }
/* Focused zone */
.xblock--drag-and-drop .zone:focus {
border: 2px solid #a5a5a5;
}
.xblock--drag-and-drop .zone p { .xblock--drag-and-drop .zone p {
visibility: hidden;
width: 100%; width: 100%;
font-family: Arial; font-family: Arial;
font-size: 16px; font-size: 16px;
...@@ -250,7 +260,7 @@ ...@@ -250,7 +260,7 @@
/*** FEEDBACK ***/ /*** FEEDBACK ***/
.xblock--drag-and-drop .feedback { .xblock--drag-and-drop .feedback {
margin-top: 20px; margin-top: 20px;
border-top: solid 1px rgb(189, 189, 189); border-top: solid 1px #bdbdbd;
} }
.xblock--drag-and-drop .popup { .xblock--drag-and-drop .popup {
...@@ -258,8 +268,8 @@ ...@@ -258,8 +268,8 @@
display: none; display: none;
top: 5%; top: 5%;
right: 5%; right: 5%;
border: 1px solid white; border: 1px solid #fff;
background: none repeat scroll 0 0 rgba(0,0,0,.8); background: none repeat scroll 0 0 rgba(0, 0, 0, 0.8);
width: 500px; width: 500px;
max-width: 90%; max-width: 90%;
min-height: 50px; min-height: 50px;
...@@ -269,7 +279,7 @@ ...@@ -269,7 +279,7 @@
} }
.xblock--drag-and-drop .popup .popup-content { .xblock--drag-and-drop .popup .popup-content {
color: #FFFFFF; color: #ffffff;
margin-left: 15px; margin-left: 15px;
margin-top: 35px; margin-top: 35px;
margin-bottom: 15px; margin-bottom: 15px;
...@@ -282,7 +292,7 @@ ...@@ -282,7 +292,7 @@
margin-right: 8px; margin-right: 8px;
margin-top: 8px; margin-top: 8px;
margin-left: 20px; margin-left: 20px;
color: #FFFFFF; color: #ffffff;
font-family: "fontawesome"; font-family: "fontawesome";
font-size: 18pt; font-size: 18pt;
} }
...@@ -290,6 +300,20 @@ ...@@ -290,6 +300,20 @@
.xblock--drag-and-drop .reset-button { .xblock--drag-and-drop .reset-button {
cursor: pointer; cursor: pointer;
float: right; float: right;
color: #3384CA; color: #2d74b3;
margin-top: 3px; margin-top: 3px;
} }
/* Make sure screen-reader content is hidden in the workbench: */
.xblock--drag-and-drop .sr {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
background: #ffffff;
color: #000000;
}
...@@ -141,9 +141,12 @@ ...@@ -141,9 +141,12 @@
width: 18%; width: 18%;
} }
.xblock--drag-and-drop--editor .zones-form .zone-row .title { .xblock--drag-and-drop--editor .zones-form .zone-row .title,
.xblock--drag-and-drop--editor .zones-form .zone-row .description {
width: 60%; width: 60%;
margin: 0 0 5px; margin: 0 0 5px;
line-height: 2.664rem; /* .title gets line-height from a Studio rule that does not apply to .description;
here we make sure that both input fields get the same value for line-height */
} }
.xblock--drag-and-drop--editor .zones-form .zone-row .layout { .xblock--drag-and-drop--editor .zones-form .zone-row .layout {
...@@ -162,6 +165,7 @@ ...@@ -162,6 +165,7 @@
height: 128px; height: 128px;
} }
.xblock--drag-and-drop--editor .target-image-form .target-image-form-help,
.xblock--drag-and-drop--editor .item-styles-form .item-styles-form-help { .xblock--drag-and-drop--editor .item-styles-form .item-styles-form-help {
margin-top: 5px; margin-top: 5px;
font-size: small; font-size: small;
...@@ -173,7 +177,7 @@ ...@@ -173,7 +177,7 @@
} }
.xblock--drag-and-drop--editor .items-form .item { .xblock--drag-and-drop--editor .items-form .item {
background: #73bde7; background: #8fcaec;
padding: 10px 0 1px; padding: 10px 0 1px;
margin: 15px 0; margin: 15px 0;
} }
...@@ -209,11 +213,12 @@ ...@@ -209,11 +213,12 @@
/** Buttons **/ /** Buttons **/
.xblock--drag-and-drop--editor .btn { .xblock--drag-and-drop--editor .btn {
background: #2e83cd; background: #2872b2;
color: #fff; color: #fff;
border: 1px solid #156ab4; border: 1px solid #156ab4;
border-radius: 6px; border-radius: 6px;
padding: 5px 10px; padding: 5px 10px;
margin-top: 15px;
} }
.xblock--drag-and-drop--editor .btn:hover { .xblock--drag-and-drop--editor .btn:hover {
...@@ -228,7 +233,7 @@ ...@@ -228,7 +233,7 @@
.xblock--drag-and-drop--editor .add-element { .xblock--drag-and-drop--editor .add-element {
text-decoration: none; text-decoration: none;
color: #2e83cd; color: #236299;
} }
.xblock--drag-and-drop--editor .remove-zone { .xblock--drag-and-drop--editor .remove-zone {
...@@ -246,7 +251,7 @@ ...@@ -246,7 +251,7 @@
width: 14px; width: 14px;
height: 14px; height: 14px;
border-radius: 7px; border-radius: 7px;
background: #2e83cd; background: #236299;
position: relative; position: relative;
float: left; float: left;
margin: 0 5px 0 0; margin: 0 5px 0 0;
...@@ -315,8 +320,9 @@ ...@@ -315,8 +320,9 @@
.xblock--drag-and-drop--editor .remove-item .icon.remove { .xblock--drag-and-drop--editor .remove-item .icon.remove {
background: #fff; background: #fff;
color: #0072a7;
} }
.xblock--drag-and-drop--editor .remove-item .icon.remove:before, .xblock--drag-and-drop--editor .remove-item .icon.remove:before,
.xblock--drag-and-drop--editor .remove-item .icon.remove:after { .xblock--drag-and-drop--editor .remove-item .icon.remove:after {
background: #2e83cd; background: #236299;
} }
...@@ -52,10 +52,11 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -52,10 +52,11 @@ function DragAndDropBlock(runtime, element, configuration) {
promise.reject(); promise.reject();
} }
}, false); }, false);
img.addEventListener("error", function() { promise.reject() }); img.addEventListener("error", function() { promise.reject(); });
img.src = configuration.target_img_expanded_url; img.src = configuration.target_img_expanded_url;
img.alt = configuration.target_img_description;
return promise; return promise;
} };
/** Zones are specified in the configuration via pixel values - convert to percentages */ /** Zones are specified in the configuration via pixel values - convert to percentages */
var computeZoneDimension = function(zone, bg_image_width, bg_image_height) { var computeZoneDimension = function(zone, bg_image_width, bg_image_height) {
...@@ -310,13 +311,17 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -310,13 +311,17 @@ function DragAndDropBlock(runtime, element, configuration) {
input.class_name = item_user_state.correct_input ? 'correct' : 'incorrect'; input.class_name = item_user_state.correct_input ? 'correct' : 'incorrect';
} }
} }
var content_html = item.displayName;
if (item.backgroundImage) {
content_html = '<img src="' + item.backgroundImage + '" alt="' + item.backgroundDescription + '" />';
}
var itemProperties = { var itemProperties = {
value: item.id, value: item.id,
drag_disabled: Boolean(item_user_state || state.finished), drag_disabled: Boolean(item_user_state || state.finished),
class_name: item_user_state && ('input' in item_user_state || item_user_state.correct_input) ? 'fade': undefined, class_name: item_user_state && ('input' in item_user_state || item_user_state.correct_input) ? 'fade': undefined,
xhr_active: (item_user_state && item_user_state.submitting_location), xhr_active: (item_user_state && item_user_state.submitting_location),
input: input, input: input,
content_html: item.backgroundImage ? '<img src="' + item.backgroundImage + '"/>' : item.displayName, content_html: content_html,
has_image: !!item.backgroundImage has_image: !!item.backgroundImage
}; };
if (item_user_state) { if (item_user_state) {
...@@ -340,6 +345,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -340,6 +345,7 @@ function DragAndDropBlock(runtime, element, configuration) {
question_html: configuration.question_text, question_html: configuration.question_text,
show_question_header: configuration.show_question_header, show_question_header: configuration.show_question_header,
target_img_src: configuration.target_img_expanded_url, target_img_src: configuration.target_img_expanded_url,
target_img_description: configuration.target_img_description,
display_zone_labels: configuration.display_zone_labels, display_zone_labels: configuration.display_zone_labels,
zones: configuration.zones, zones: configuration.zones,
items: items, items: items,
......
...@@ -70,9 +70,11 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -70,9 +70,11 @@ function DragAndDropEditBlock(runtime, element, params) {
} }
// Set the target image and bind its event handler: // Set the target image and bind its event handler:
$('.target-image-form input', element).val(_fn.data.targetImg); $('.target-image-form input.url-input', element).val(_fn.data.targetImg);
$('.target-image-form input.description-input', element).val(_fn.data.targetImgDescription);
_fn.build.$el.targetImage.load(_fn.build.form.zone.imageLoaded); _fn.build.$el.targetImage.load(_fn.build.form.zone.imageLoaded);
_fn.build.$el.targetImage.attr('src', params.target_img_expanded_url); _fn.build.$el.targetImage.attr('src', params.target_img_expanded_url);
_fn.build.$el.targetImage.attr('alt', params.target_img_description);
if (_fn.data.displayLabels) { if (_fn.data.displayLabels) {
$('.display-labels-form input', element).prop('checked', true); $('.display-labels-form input', element).prop('checked', true);
...@@ -122,7 +124,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -122,7 +124,7 @@ function DragAndDropEditBlock(runtime, element, params) {
.on('click', '.target-image-form button', function(e) { .on('click', '.target-image-form button', function(e) {
e.preventDefault(); e.preventDefault();
var new_img_url = $.trim($('.target-image-form input', element).val()); var new_img_url = $.trim($('.target-image-form input.url-input', element).val());
if (new_img_url) { if (new_img_url) {
// We may need to 'expand' the URL before it will be valid. // We may need to 'expand' the URL before it will be valid.
// e.g. '/static/blah.png' becomes '/asset-v1:course+id/blah.png' // e.g. '/static/blah.png' becomes '/asset-v1:course+id/blah.png'
...@@ -136,6 +138,12 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -136,6 +138,12 @@ function DragAndDropEditBlock(runtime, element, params) {
} }
_fn.data.targetImg = new_img_url; _fn.data.targetImg = new_img_url;
var new_description = $.trim(
$('.target-image-form input.description-input', element).val()
);
_fn.build.$el.targetImage.attr('alt', new_description);
_fn.data.targetImgDescription = new_description;
// Placeholder shim for IE9 // Placeholder shim for IE9
$.placeholder.shim(); $.placeholder.shim();
}) })
...@@ -176,6 +184,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -176,6 +184,7 @@ function DragAndDropEditBlock(runtime, element, params) {
// Update zone obj // Update zone obj
var zoneObj = { var zoneObj = {
title: oldZone.title || 'Zone ' + num, title: oldZone.title || 'Zone ' + num,
description: oldZone.description,
id: name, id: name,
index: num, index: num,
width: oldZone.width || 200, width: oldZone.width || 200,
...@@ -247,6 +256,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -247,6 +256,7 @@ function DragAndDropEditBlock(runtime, element, params) {
_fn.tpl.zoneElement({ _fn.tpl.zoneElement({
id: zoneObj.id, id: zoneObj.id,
title: zoneObj.title, title: zoneObj.title,
description: zoneObj.description,
x_percent: (+zoneObj.x) / imgWidth * 100, x_percent: (+zoneObj.x) / imgWidth * 100,
y_percent: (+zoneObj.y) / imgHeight * 100, y_percent: (+zoneObj.y) / imgHeight * 100,
width_percent: (+zoneObj.width) / imgWidth * 100, width_percent: (+zoneObj.width) / imgWidth * 100,
...@@ -276,6 +286,8 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -276,6 +286,8 @@ function DragAndDropEditBlock(runtime, element, params) {
record.title = $changedInput.val(); record.title = $changedInput.val();
} else if ($changedInput.hasClass('width')) { } else if ($changedInput.hasClass('width')) {
record.width = $changedInput.val(); record.width = $changedInput.val();
} else if ($changedInput.hasClass('description')) {
record.description = $changedInput.val();
} else if ($changedInput.hasClass('height')) { } else if ($changedInput.hasClass('height')) {
record.height = $changedInput.val(); record.height = $changedInput.val();
} else if ($changedInput.hasClass('x')) { } else if ($changedInput.hasClass('x')) {
...@@ -376,7 +388,8 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -376,7 +388,8 @@ function DragAndDropEditBlock(runtime, element, params) {
$form.each(function(i, el) { $form.each(function(i, el) {
var $el = $(el), var $el = $(el),
name = $el.find('.item-text').val(), name = $el.find('.item-text').val(),
backgroundImage = $el.find('.background-image').val(); backgroundImage = $el.find('.background-image-url').val(),
backgroundDescription = $el.find('.background-image-description').val();
if (name.length > 0 || backgroundImage.length > 0) { if (name.length > 0 || backgroundImage.length > 0) {
// Item width/height are ignored, but preserve the data: // Item width/height are ignored, but preserve the data:
...@@ -401,7 +414,8 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -401,7 +414,8 @@ function DragAndDropEditBlock(runtime, element, params) {
width: width, width: width,
height: height height: height
}, },
backgroundImage: backgroundImage backgroundImage: backgroundImage,
backgroundDescription: backgroundDescription
}; };
var numValue = parseFloat($el.find('.item-numerical-value').val()); var numValue = parseFloat($el.find('.item-numerical-value').val());
...@@ -410,7 +424,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -410,7 +424,7 @@ function DragAndDropEditBlock(runtime, element, params) {
data.inputOptions = { data.inputOptions = {
value: numValue, value: numValue,
margin: isFinite(numMargin) ? numMargin : 0 margin: isFinite(numMargin) ? numMargin : 0
} };
} }
items.push(data); items.push(data);
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
var itemTemplate = function(item) { var itemTemplate = function(item) {
var style = {}; var style = {};
var className = (item.class_name) ? item.class_name : ""; var className = (item.class_name) ? item.class_name : "";
var tabindex = 0;
if (item.background_color) { if (item.background_color) {
style['background-color'] = item.background_color; style['background-color'] = item.background_color;
} }
...@@ -64,6 +65,8 @@ ...@@ -64,6 +65,8 @@
if (item.is_placed) { if (item.is_placed) {
style.left = item.x_percent + "%"; style.left = item.x_percent + "%";
style.top = item.y_percent + "%"; style.top = item.y_percent + "%";
tabindex = -1; // If an item has been placed it can no longer be interacted with,
// so remove the ability to move focus to it using the keyboard
} }
if (item.has_image) { if (item.has_image) {
className += " " + "option-with-image"; className += " " + "option-with-image";
...@@ -73,7 +76,13 @@ ...@@ -73,7 +76,13 @@
{ {
key: item.value, key: item.value,
className: className, className: className,
attributes: {'data-value': item.value, 'data-drag-disabled': item.drag_disabled}, attributes: {
'tabindex': tabindex,
'draggable': true,
'aria-grabbed': false,
'data-value': item.value,
'data-drag-disabled': item.drag_disabled
},
style: style style: style
}, [ }, [
itemSpinnerTemplate(item.xhr_active), itemSpinnerTemplate(item.xhr_active),
...@@ -85,18 +94,27 @@ ...@@ -85,18 +94,27 @@
}; };
var zoneTemplate = function(zone, ctx) { var zoneTemplate = function(zone, ctx) {
var className = ctx.display_zone_labels ? 'zone-name' : 'zone-name sr';
return ( return (
h( h(
'div.zone', 'div.zone',
{ {
id: zone.id, id: zone.id,
attributes: {'data-zone': zone.title}, attributes: {
'tabindex': 0,
'dropzone': 'move',
'aria-dropeffect': 'move',
'data-zone': zone.title
},
style: { style: {
top: zone.y_percent + '%', left: zone.x_percent + "%", top: zone.y_percent + '%', left: zone.x_percent + "%",
width: zone.width_percent + '%', height: zone.height_percent + "%", width: zone.width_percent + '%', height: zone.height_percent + "%",
} }
}, },
ctx.display_zone_labels ? h('p', zone.title) : null [
h('p', { className: className }, zone.title),
h('p', { className: 'zone-description sr' }, zone.description)
]
) )
); );
}; };
...@@ -104,8 +122,9 @@ ...@@ -104,8 +122,9 @@
var feedbackTemplate = function(ctx) { var feedbackTemplate = function(ctx) {
var feedback_display = ctx.feedback_html ? 'block' : 'none'; var feedback_display = ctx.feedback_html ? 'block' : 'none';
var reset_button_display = ctx.display_reset_button ? 'block' : 'none'; var reset_button_display = ctx.display_reset_button ? 'block' : 'none';
var properties = { attributes: { 'aria-live': 'polite' } };
return ( return (
h('section.feedback', [ h('section.feedback', properties, [
h('div.reset-button', {style: {display: reset_button_display}}, gettext('Reset exercise')), h('div.reset-button', {style: {display: reset_button_display}}, gettext('Reset exercise')),
h('h3.title1', {style: {display: feedback_display}}, gettext('Feedback')), h('h3.title1', {style: {display: feedback_display}}, gettext('Feedback')),
h('p.message', {style: {display: feedback_display}, h('p.message', {style: {display: feedback_display},
...@@ -135,7 +154,7 @@ ...@@ -135,7 +154,7 @@
h('p.popup-content', {innerHTML: ctx.popup_html}), h('p.popup-content', {innerHTML: ctx.popup_html}),
]), ]),
h('div.target-img-wrapper', [ h('div.target-img-wrapper', [
h('img.target-img', {src: ctx.target_img_src, alt: "Image Description here"}), h('img.target-img', {src: ctx.target_img_src, alt: ctx.target_img_description}),
]), ]),
renderCollection(zoneTemplate, ctx.zones, ctx), renderCollection(zoneTemplate, ctx.zones, ctx),
renderCollection(itemTemplate, items_placed, ctx), renderCollection(itemTemplate, items_placed, ctx),
......
...@@ -41,18 +41,39 @@ ...@@ -41,18 +41,39 @@
<div class="tab zones-tab hidden"> <div class="tab zones-tab hidden">
<header class="tab-header"> <header class="tab-header">
<h3>{% trans "Zone Positions" %}</h3> <h3>{% trans "Zones" %}</h3>
</header> </header>
<section class="tab-content"> <section class="tab-content">
<section class="tab-content target-image-form"> <form class="target-image-form">
<label>{% trans "New background URL" %}:</label> <h3 id="background-url-label">
<input type="text"> {% trans "Background URL" %}
</h3>
<input type="text" class="url-input" aria-labelledby="background-url-label">
<h3 id="background-description-label">
{% trans "Background description" %}
</h3>
<input type="text"
class="description-input"
aria-labelledby="background-description-label"
aria-describedby="background-description-description">
<div id="background-description-description" class="target-image-form-help">
{% blocktrans %}
Please provide a description of the image for non-visual users.
The description should provide sufficient information that would allow anyone
to solve the problem if the image did not load.
{% endblocktrans %}
</div>
<button class="btn">{% trans "Change background" %}</button> <button class="btn">{% trans "Change background" %}</button>
</form>
</section> </section>
<section class="tab-content display-labels-form"> <section class="tab-content">
<form class="display-labels-form">
<h3>{% trans "Zone labels" %}</h3>
<label for="display-labels">{% trans "Display label names on the image" %}:</label> <label for="display-labels">{% trans "Display label names on the image" %}:</label>
<input name="display-labels" id="display-labels" type="checkbox" /> <input name="display-labels" id="display-labels" type="checkbox" />
</form>
</section> </section>
<section class="tab-content">
<div class="zone-editor"> <div class="zone-editor">
<div class="controls"> <div class="controls">
<form class="zones-form"></form> <form class="zones-form"></form>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
width:{{ width_percent }}%; width:{{ width_percent }}%;
height:{{ height_percent }}%;"> height:{{ height_percent }}%;">
<p>{{{ title }}}</p> <p>{{{ title }}}</p>
<p class="sr">{{{ description }}}</p>
</div> </div>
</script> </script>
...@@ -15,6 +16,11 @@ ...@@ -15,6 +16,11 @@
<a href="#" class="remove-zone hidden"> <a href="#" class="remove-zone hidden">
<div class="icon remove"></div> <div class="icon remove"></div>
</a> </a>
<label>{{i18n "Description"}}</label>
<input type="text"
class="description"
value="{{ description }}"
placeholder="{{i18n 'Describe this zone to non-visual users'}}" />
<div class="layout"> <div class="layout">
<label>{{i18n "width"}}</label> <label>{{i18n "width"}}</label>
<input type="text" class="size width" value="{{ width }}" /> <input type="text" class="size width" value="{{ width }}" />
...@@ -46,7 +52,11 @@ ...@@ -46,7 +52,11 @@
</div> </div>
<div class="row"> <div class="row">
<label>{{i18n "Background image URL (alternative to the text)"}}</label> <label>{{i18n "Background image URL (alternative to the text)"}}</label>
<textarea class="background-image">{{ backgroundImage }}</textarea> <textarea class="background-image-url">{{ backgroundImage }}</textarea>
</div>
<div class="row">
<label>{{i18n "Background image description (should provide sufficient information to place the item even if the image did not load)"}}</label>
<textarea class="background-image-description">{{ backgroundDescription }}</textarea>
</div> </div>
<div class="row"> <div class="row">
<label>{{i18n "Success Feedback"}}</label> <label>{{i18n "Success Feedback"}}</label>
......
{
"zones": [
{
"index": 1,
"title": "Zone 1",
"description": "This describes zone 1",
"height": 178,
"width": 196,
"y": "30",
"x": "160",
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"description": "This describes zone 2",
"height": 140,
"width": 340,
"y": "210",
"x": "86",
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"backgroundImage": "https://placehold.it/100x100",
"backgroundDescription": "This describes the background image of item 1",
"feedback": {
"incorrect": "No, 1 does not belong here",
"correct": "Yes, 1 goes here"
},
"zone": "Zone 1",
"id": 0
},
{
"displayName": "2",
"backgroundImage": "https://placehold.it/100x100",
"backgroundDescription": "This describes the background image of item 2",
"feedback": {
"incorrect": "No, 2 does not belong here",
"correct": "Yes, 2 goes here"
},
"zone": "Zone 2",
"id": 1
},
{
"displayName": "X",
"backgroundImage": "",
"feedback": {
"incorrect": "You silly, there are no zones for X",
"correct": ""
},
"zone": "none",
"id": 2
}
],
"feedback": {
"start": "Drag the items onto the image above.",
"finish": "Good work! You have completed this drag and drop exercise."
},
"targetImgDescription": "This describes the target image"
}
...@@ -59,5 +59,6 @@ ...@@ -59,5 +59,6 @@
"start": "Intro <i>Feed</i>", "start": "Intro <i>Feed</i>",
"finish": "Final <b>Feed</b>" "finish": "Final <b>Feed</b>"
}, },
"targetImg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==" "targetImg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==",
"targetImgDescription": "This describes the target image"
} }
...@@ -57,6 +57,9 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -57,6 +57,9 @@ class BaseIntegrationTest(SeleniumBaseTest):
def _get_zones(self): def _get_zones(self):
return self._page.find_elements_by_css_selector(".drag-container .zone") return self._page.find_elements_by_css_selector(".drag-container .zone")
def _get_feedback(self):
return self._page.find_element_by_css_selector(".feedback")
def _get_feedback_message(self): def _get_feedback_message(self):
return self._page.find_element_by_css_selector(".feedback .message") return self._page.find_element_by_css_selector(".feedback .message")
......
...@@ -31,4 +31,6 @@ class TestCustomDataDragAndDropRendering(BaseIntegrationTest): ...@@ -31,4 +31,6 @@ class TestCustomDataDragAndDropRendering(BaseIntegrationTest):
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciI" "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciI"
"HdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==" "HdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg=="
) )
custom_image_description = "This describes the target image"
self.assertEqual(bg_image.get_attribute("src"), custom_image_url) self.assertEqual(bg_image.get_attribute("src"), custom_image_url)
self.assertEqual(bg_image.get_attribute("alt"), custom_image_description)
...@@ -145,7 +145,7 @@ class InteractionTestBase(object): ...@@ -145,7 +145,7 @@ class InteractionTestBase(object):
self.assertDictEqual(locations_after_reset[item_key], initial_locations[item_key]) self.assertDictEqual(locations_after_reset[item_key], initial_locations[item_key])
class InteractionTestFixture(InteractionTestBase): class BasicInteractionTest(InteractionTestBase):
""" """
Verifying Drag and Drop XBlock rendering against default data - if default data changes this will probably break. Verifying Drag and Drop XBlock rendering against default data - if default data changes this will probably break.
""" """
...@@ -184,7 +184,7 @@ class InteractionTestFixture(InteractionTestBase): ...@@ -184,7 +184,7 @@ class InteractionTestFixture(InteractionTestBase):
self.parameterized_final_feedback_and_reset(self.items_map, self.feedback) self.parameterized_final_feedback_and_reset(self.items_map, self.feedback)
class CustomDataInteractionTest(InteractionTestFixture, BaseIntegrationTest): class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
items_map = { items_map = {
0: ItemDefinition(0, 'Zone 1', "Yes 1", "No 1"), 0: ItemDefinition(0, 'Zone 1', "Yes 1", "No 1"),
1: ItemDefinition(1, 'Zone 2', "Yes 2", "No 2", "102"), 1: ItemDefinition(1, 'Zone 2', "Yes 2", "No 2", "102"),
...@@ -202,7 +202,7 @@ class CustomDataInteractionTest(InteractionTestFixture, BaseIntegrationTest): ...@@ -202,7 +202,7 @@ class CustomDataInteractionTest(InteractionTestFixture, BaseIntegrationTest):
return self._get_custom_scenario_xml("integration/data/test_data.json") return self._get_custom_scenario_xml("integration/data/test_data.json")
class CustomHtmlDataInteractionTest(InteractionTestFixture, BaseIntegrationTest): class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
items_map = { items_map = {
0: ItemDefinition(0, 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"), 0: ItemDefinition(0, 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"),
1: ItemDefinition(1, 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>", "95"), 1: ItemDefinition(1, 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>", "95"),
......
from ddt import ddt, unpack, data from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException
from ..utils import load_resource
from .test_base import BaseIntegrationTest from .test_base import BaseIntegrationTest
class Colors(object): class Colors(object):
WHITE = 'rgb(255, 255, 255)' WHITE = 'rgb(255, 255, 255)'
BLUE = 'rgb(46, 131, 205)' BLUE = 'rgb(40, 114, 178)'
GREY = 'rgb(237, 237, 237)' GREY = 'rgb(237, 237, 237)'
CORAL = '#ff7f50' CORAL = '#ff7f50'
CORNFLOWERBLUE = 'cornflowerblue' CORNFLOWERBLUE = 'cornflowerblue'
...@@ -32,10 +34,16 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -32,10 +34,16 @@ class TestDragAndDropRender(BaseIntegrationTest):
def load_scenario(self, item_background_color="", item_text_color=""): def load_scenario(self, item_background_color="", item_text_color=""):
scenario_xml = """ scenario_xml = """
<vertical_demo> <vertical_demo>
<drag-and-drop-v2 item_background_color='{item_background_color}' item_text_color='{item_text_color}' /> <drag-and-drop-v2 item_background_color='{item_background_color}'
</vertical_demo> item_text_color='{item_text_color}'
""".format(item_background_color=item_background_color, item_text_color=item_text_color) data='{data}' />
</vertical_demo>
""".format(
item_background_color=item_background_color,
item_text_color=item_text_color,
data=load_resource("integration/data/test_data_a11y.json")
)
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml) self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml)
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
...@@ -83,18 +91,9 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -83,18 +91,9 @@ class TestDragAndDropRender(BaseIntegrationTest):
color = self._get_style('.item-bank .option[data-value='+item_val+']', 'color') color = self._get_style('.item-bank .option[data-value='+item_val+']', 'color')
self.assertEquals(color, expected_color) self.assertEquals(color, expected_color)
def test_items(self): def test_items_default_colors(self):
self.load_scenario() self.load_scenario()
self._test_items()
items = self._get_items()
self.assertEqual(len(items), 3)
for index, item in enumerate(items):
self.assertEqual(item.get_attribute('data-value'), str(index))
self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text'])
self.assertIn('ui-draggable', self.get_element_classes(item))
self._test_item_style(item, {})
@unpack @unpack
@data( @data(
...@@ -105,21 +104,36 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -105,21 +104,36 @@ class TestDragAndDropRender(BaseIntegrationTest):
def test_items_custom_colors(self, item_background_color, item_text_color): def test_items_custom_colors(self, item_background_color, item_text_color):
self.load_scenario(item_background_color, item_text_color) self.load_scenario(item_background_color, item_text_color)
items = self._get_items()
self.assertEqual(len(items), 3)
color_settings = {} color_settings = {}
if item_background_color: if item_background_color:
color_settings['background-color'] = item_background_color color_settings['background-color'] = item_background_color
if item_text_color: if item_text_color:
color_settings['color'] = item_text_color color_settings['color'] = item_text_color
self._test_items(color_settings=color_settings)
def _test_items(self, color_settings={}): # pylint: disable=dangerous-default-value
items = self._get_items()
self.assertEqual(len(items), 3)
for index, item in enumerate(items): for index, item in enumerate(items):
item_number = index + 1
self.assertEqual(item.get_attribute('tabindex'), '0')
self.assertEqual(item.get_attribute('draggable'), 'true')
self.assertEqual(item.get_attribute('aria-grabbed'), 'false')
self.assertEqual(item.get_attribute('data-value'), str(index)) self.assertEqual(item.get_attribute('data-value'), str(index))
self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text'])
self.assertIn('ui-draggable', self.get_element_classes(item)) self.assertIn('ui-draggable', self.get_element_classes(item))
self._test_item_style(item, color_settings) self._test_item_style(item, color_settings)
try:
background_image = item.find_element_by_css_selector('img')
except NoSuchElementException:
self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text'])
else:
self.assertEqual(
background_image.get_attribute('alt'),
'This describes the background image of item {}'.format(item_number)
)
def test_zones(self): def test_zones(self):
self.load_scenario() self.load_scenario()
...@@ -128,19 +142,39 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -128,19 +142,39 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.assertEqual(len(zones), 2) self.assertEqual(len(zones), 2)
self.assertEqual(zones[0].get_attribute('data-zone'), 'Zone 1') box_percentages = [
self.assertIn('ui-droppable', self.get_element_classes(zones[0])) {"left": 31.1284, "top": 6.17284, "width": 38.1323, "height": 36.6255},
self._assert_box_percentages('#zone-1', left=31.1284, top=6.17284, width=38.1323, height=36.6255) {"left": 16.7315, "top": 43.2099, "width": 66.1479, "height": 28.8066}
]
self.assertEqual(zones[1].get_attribute('data-zone'), 'Zone 2')
self.assertIn('ui-droppable', self.get_element_classes(zones[1])) for index, zone in enumerate(zones):
self._assert_box_percentages('#zone-2', left=16.7315, top=43.2099, width=66.1479, height=28.8066) zone_number = index + 1
self.assertEqual(zone.get_attribute('tabindex'), '0')
self.assertEqual(zone.get_attribute('dropzone'), 'move')
self.assertEqual(zone.get_attribute('aria-dropeffect'), 'move')
self.assertEqual(zone.get_attribute('data-zone'), 'Zone {}'.format(zone_number))
self.assertIn('ui-droppable', self.get_element_classes(zone))
zone_box_percentages = box_percentages[index]
self._assert_box_percentages(
'#zone-{}'.format(zone_number),
left=zone_box_percentages['left'],
top=zone_box_percentages['top'],
width=zone_box_percentages['width'],
height=zone_box_percentages['height']
)
zone_name = zone.find_element_by_css_selector('p.zone-name')
self.assertEqual(zone_name.text, 'ZONE {}'.format(zone_number))
zone_description = zone.find_element_by_css_selector('p.zone-description')
self.assertEqual(zone_description.text, 'THIS DESCRIBES ZONE {}'.format(zone_number))
# Zone description should only be visible to screen readers:
self.assertEqual(zone_description.get_attribute('class'), 'zone-description sr')
def test_feedback(self): def test_feedback(self):
self.load_scenario() self.load_scenario()
feedback = self._get_feedback()
feedback_message = self._get_feedback_message() feedback_message = self._get_feedback_message()
self.assertEqual(feedback.get_attribute('aria-live'), 'polite')
self.assertEqual(feedback_message.text, "Drag the items onto the image above.") self.assertEqual(feedback_message.text, "Drag the items onto the image above.")
def test_background_image(self): def test_background_image(self):
...@@ -149,3 +183,4 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -149,3 +183,4 @@ class TestDragAndDropRender(BaseIntegrationTest):
bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img") bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img")
image_path = '/resource/drag-and-drop-v2/public/img/triangle.png' image_path = '/resource/drag-and-drop-v2/public/img/triangle.png'
self.assertTrue(bg_image.get_attribute("src").endswith(image_path)) self.assertTrue(bg_image.get_attribute("src").endswith(image_path))
self.assertEqual(bg_image.get_attribute("alt"), 'This describes the target image')
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"question_text": "Solve this <strong>drag-and-drop</strong> problem.", "question_text": "Solve this <strong>drag-and-drop</strong> problem.",
"show_question_header": false, "show_question_header": false,
"target_img_expanded_url": "/expanded/url/to/drag_and_drop_v2/public/img/triangle.png", "target_img_expanded_url": "/expanded/url/to/drag_and_drop_v2/public/img/triangle.png",
"target_img_description": "This describes the target image",
"item_background_color": "white", "item_background_color": "white",
"item_text_color": "#000080", "item_text_color": "#000080",
"initial_feedback": "HTML <strong>Intro</strong> Feed", "initial_feedback": "HTML <strong>Intro</strong> Feed",
......
...@@ -69,5 +69,6 @@ ...@@ -69,5 +69,6 @@
"feedback": { "feedback": {
"start": "HTML <strong>Intro</strong> Feed", "start": "HTML <strong>Intro</strong> Feed",
"finish": "Final <strong>feedback</strong>!" "finish": "Final <strong>feedback</strong>!"
} },
"targetImgDescription": "This describes the target image"
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"question_text": "", "question_text": "",
"show_question_header": true, "show_question_header": true,
"target_img_expanded_url": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png", "target_img_expanded_url": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"target_img_description": "This describes the target image",
"item_background_color": null, "item_background_color": null,
"item_text_color": null, "item_text_color": null,
"initial_feedback": "Intro Feed", "initial_feedback": "Intro Feed",
......
...@@ -89,5 +89,6 @@ ...@@ -89,5 +89,6 @@
"start": "Intro Feed", "start": "Intro Feed",
"finish": "Final Feed" "finish": "Final Feed"
}, },
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png" "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"targetImgDescription": "This describes the target image"
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"question_text": "Can you solve this drag-and-drop problem?", "question_text": "Can you solve this drag-and-drop problem?",
"show_question_header": true, "show_question_header": true,
"target_img_expanded_url": "http://placehold.it/800x600", "target_img_expanded_url": "http://placehold.it/800x600",
"target_img_description": "This describes the target image",
"item_background_color": null, "item_background_color": null,
"item_text_color": null, "item_text_color": null,
"initial_feedback": "This is the initial feedback.", "initial_feedback": "This is the initial feedback.",
......
...@@ -76,5 +76,6 @@ ...@@ -76,5 +76,6 @@
"targetImg": "http://placehold.it/800x600", "targetImg": "http://placehold.it/800x600",
"targetImgDescription": "This describes the target image",
"displayLabels": false "displayLabels": false
} }
...@@ -36,6 +36,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -36,6 +36,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
"question_text": "", "question_text": "",
"show_question_header": True, "show_question_header": True,
"target_img_expanded_url": '/expanded/url/to/drag_and_drop_v2/public/img/triangle.png', "target_img_expanded_url": '/expanded/url/to/drag_and_drop_v2/public/img/triangle.png',
"target_img_description": "",
"item_background_color": None, "item_background_color": None,
"item_text_color": None, "item_text_color": None,
"initial_feedback": DEFAULT_START_FEEDBACK, "initial_feedback": DEFAULT_START_FEEDBACK,
......
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