Commit a9375179 by Matjaz Gregoric

Use dedicated aria-live region for SR message.

Implement our own SR message area instead of relying on SR tools from
edX LMS, so that screen reader messages work even when this xblock is
used outside of LMS.
parent 5383bf4b
......@@ -569,6 +569,10 @@ function DragAndDropTemplates(configuration) {
]),
keyboardHelpPopupTemplate(ctx),
feedbackTemplate(ctx),
h('div.sr.reader-feedback-area', {
attributes: {'aria-live': 'polite', 'aria-atomic': true},
innerHTML: ctx.screen_reader_messages
}),
])
);
};
......@@ -931,36 +935,49 @@ function DragAndDropBlock(runtime, element, configuration) {
__vdom = new_vdom;
};
// Uses edX JS accessibility tools to read feedback messages when present.
var readScreenReaderMessages = function() {
if (window.SR && window.SR.readText) {
var pluckMessages = function(feedback_items) {
return feedback_items.map(function(item) {
return item.message;
});
};
var messages = [];
// In standard mode, it makes more sense to read the per-item feedback before overall feedback.
if (state.feedback && configuration.mode === DragAndDropBlock.STANDARD_MODE) {
messages = messages.concat(pluckMessages(state.feedback));
}
if (state.overall_feedback) {
messages = messages.concat(pluckMessages(state.overall_feedback));
}
// In assessment mode overall feedback comes first then multiple per-item feedbacks.
if (state.feedback && configuration.mode === DragAndDropBlock.ASSESSMENT_MODE) {
if (state.feedback.length > 0) {
if (!state.last_action_correct) {
messages.push(gettext("Some of your answers were not correct."));
}
messages = messages.concat(
gettext("Hints:"),
pluckMessages(state.feedback)
);
var sr_clear_timeout = null;
var setScreenReaderMessages = function() {
clearTimeout(sr_clear_timeout);
var pluckMessages = function(feedback_items) {
return feedback_items.map(function(item) {
return item.message;
});
};
var messages = [];
// In standard mode, it makes more sense to read the per-item feedback before overall feedback.
if (state.feedback && configuration.mode === DragAndDropBlock.STANDARD_MODE) {
messages = messages.concat(pluckMessages(state.feedback));
}
if (state.overall_feedback) {
messages = messages.concat(pluckMessages(state.overall_feedback));
}
// In assessment mode overall feedback comes first then multiple per-item feedbacks.
if (state.feedback && configuration.mode === DragAndDropBlock.ASSESSMENT_MODE) {
if (state.feedback.length > 0) {
if (!state.last_action_correct) {
messages.push(gettext("Some of your answers were not correct."));
}
messages = messages.concat(
gettext("Hints:"),
pluckMessages(state.feedback)
);
}
SR.readText(messages.join('\n'));
}
var paragraphs = messages.map(function(msg) {
return '<p>' + msg + '</p>';
});
state.screen_reader_messages = paragraphs.join('');
// Remove the text on next redraw. This will make screen readers read the message again,
// next time the user performs an action, even if next feedback message did not change from
// last attempt (for example: if user drops the same item on two wrong zones one after another,
// the negative feedback should be read out twice, not only on first drop).
sr_clear_timeout = setTimeout(function() {
state.screen_reader_messages = '';
}, 0);
};
var publishEvent = function(data) {
......@@ -1267,7 +1284,7 @@ function DragAndDropBlock(runtime, element, configuration) {
state.finished = true;
state.overall_feedback = data.overall_feedback;
}
readScreenReaderMessages();
setScreenReaderMessages();
}
applyState();
if (state.feedback && state.feedback.length > 0) {
......@@ -1372,7 +1389,7 @@ function DragAndDropBlock(runtime, element, configuration) {
} else {
state.finished = true;
}
readScreenReaderMessages();
setScreenReaderMessages();
}).always(function() {
state.submit_spinner = false;
applyState();
......@@ -1500,7 +1517,8 @@ function DragAndDropBlock(runtime, element, configuration) {
showing_answer: state.showing_answer,
show_answer_spinner: state.show_answer_spinner,
disable_go_to_beginning_button: !canGoToBeginning(),
show_go_to_beginning_button: state.go_to_beginning_button_visible
show_go_to_beginning_button: state.go_to_beginning_button_visible,
screen_reader_messages: (state.screen_reader_messages || '')
};
return renderView(context);
......
......@@ -163,27 +163,6 @@ class BaseIntegrationTest(SeleniumBaseTest):
focused_element = self.browser.switch_to.active_element
self.assertTrue(element != focused_element, 'expected element to not have focus')
def _patch_sr_read_text(self):
"""
Creates a mock SR.readText function that stores submitted text into a global variable
for later inspection.
Returns a getter function that returns stored SR texts.
"""
self.browser.execute_script(
"""
window.SR = {
received_texts: [],
readText: function(text) {
window.SR.received_texts.push(text);
}
};
"""
)
def get_sr_texts():
return self.browser.execute_script('return window.SR.received_texts')
return get_sr_texts
@staticmethod
def get_element_html(element):
return element.get_attribute('innerHTML').strip()
......@@ -510,3 +489,10 @@ class InteractionTestBase(object):
def assert_button_enabled(self, submit_button, enabled=True):
self.assertEqual(submit_button.is_enabled(), enabled)
def assert_reader_feedback_messages(self, expected_message_lines):
expected_paragraphs = ['<p>{}</p>'.format(l) for l in expected_message_lines]
expected_html = ''.join(expected_paragraphs)
feedback_area = self._page.find_element_by_css_selector('.reader-feedback-area')
actual_html = feedback_area.get_attribute('innerHTML')
self.assertEqual(actual_html, expected_html)
......@@ -56,7 +56,6 @@ class ParameterizedTestsMixin(object):
if feedback is None:
feedback = self.feedback
get_sr_texts = self._patch_sr_read_text()
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
......@@ -79,7 +78,7 @@ class ParameterizedTestsMixin(object):
else:
overall_feedback = feedback['intro']
expected_sr_texts.append(overall_feedback)
self.assertEqual(get_sr_texts()[-1], '\n'.join(expected_sr_texts))
self.assert_reader_feedback_messages(expected_sr_texts)
if action_key:
# Next TAB keypress should move focus to "Go to Beginning button"
self._test_next_tab_goes_to_go_to_beginning_button()
......@@ -90,7 +89,6 @@ class ParameterizedTestsMixin(object):
if feedback is None:
feedback = self.feedback
get_sr_texts = self._patch_sr_read_text()
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
......@@ -105,7 +103,7 @@ class ParameterizedTestsMixin(object):
feedback_popup_html = feedback_popup_content.get_attribute('innerHTML')
self.assertEqual(feedback_popup_html, '')
self.assertFalse(popup.is_displayed())
self.assertEqual(get_sr_texts(), [])
self.assert_reader_feedback_messages([])
def parameterized_item_negative_feedback_on_bad_move_standard(
self, items_map, all_zones, scroll_down=100, action_key=None, feedback=None
......@@ -113,7 +111,6 @@ class ParameterizedTestsMixin(object):
if feedback is None:
feedback = self.feedback
get_sr_texts = self._patch_sr_read_text()
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
......@@ -129,7 +126,7 @@ class ParameterizedTestsMixin(object):
self.assertTrue(popup.is_displayed())
self.assert_reverted_item(definition.item_id)
expected_sr_texts = [definition.feedback_negative, feedback['intro']]
self.assertEqual(get_sr_texts()[-1], '\n'.join(expected_sr_texts))
self.assert_reader_feedback_messages(expected_sr_texts)
self._test_popup_focus_and_close(popup, action_key)
def parameterized_item_negative_feedback_on_bad_move_assessment(
......@@ -138,7 +135,6 @@ class ParameterizedTestsMixin(object):
if feedback is None:
feedback = self.feedback
get_sr_texts = self._patch_sr_read_text()
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
......@@ -154,7 +150,7 @@ class ParameterizedTestsMixin(object):
self.assertEqual(feedback_popup_html, '')
self.assertFalse(popup.is_displayed())
self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True)
self.assertEqual(get_sr_texts(), [])
self.assert_reader_feedback_messages([])
self._test_popup_focus_and_close(popup, action_key)
if action_key:
self._test_next_tab_goes_to_go_to_beginning_button()
......
......@@ -248,8 +248,6 @@ class AssessmentInteractionTest(
"""
Test updating overall feedback after submitting solution in assessment mode
"""
get_sr_texts = self._patch_sr_read_text()
def check_feedback(overall_feedback_lines, per_item_feedback_lines=None):
# Check that the feedback is correctly displayed in the overall feedback area.
expected_overall_feedback = "\n".join(["FEEDBACK"] + overall_feedback_lines)
......@@ -260,7 +258,7 @@ class AssessmentInteractionTest(
if per_item_feedback_lines:
sr_feedback_lines += ["Some of your answers were not correct.", "Hints:"]
sr_feedback_lines += per_item_feedback_lines
self.assertEqual(get_sr_texts()[-1], '\n'.join(sr_feedback_lines))
self.assert_reader_feedback_messages(sr_feedback_lines)
# used keyboard mode to avoid bug/feature with selenium "selecting" everything instead of dragging an element
self.place_item(0, TOP_ZONE_ID, Keys.RETURN)
......
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