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) { ...@@ -569,6 +569,10 @@ function DragAndDropTemplates(configuration) {
]), ]),
keyboardHelpPopupTemplate(ctx), keyboardHelpPopupTemplate(ctx),
feedbackTemplate(ctx), feedbackTemplate(ctx),
h('div.sr.reader-feedback-area', {
attributes: {'aria-live': 'polite', 'aria-atomic': true},
innerHTML: ctx.screen_reader_messages
}),
]) ])
); );
}; };
...@@ -931,9 +935,11 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -931,9 +935,11 @@ function DragAndDropBlock(runtime, element, configuration) {
__vdom = new_vdom; __vdom = new_vdom;
}; };
// Uses edX JS accessibility tools to read feedback messages when present. var sr_clear_timeout = null;
var readScreenReaderMessages = function() {
if (window.SR && window.SR.readText) { var setScreenReaderMessages = function() {
clearTimeout(sr_clear_timeout);
var pluckMessages = function(feedback_items) { var pluckMessages = function(feedback_items) {
return feedback_items.map(function(item) { return feedback_items.map(function(item) {
return item.message; return item.message;
...@@ -959,8 +965,19 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -959,8 +965,19 @@ function DragAndDropBlock(runtime, element, configuration) {
); );
} }
} }
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) { var publishEvent = function(data) {
...@@ -1267,7 +1284,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1267,7 +1284,7 @@ function DragAndDropBlock(runtime, element, configuration) {
state.finished = true; state.finished = true;
state.overall_feedback = data.overall_feedback; state.overall_feedback = data.overall_feedback;
} }
readScreenReaderMessages(); setScreenReaderMessages();
} }
applyState(); applyState();
if (state.feedback && state.feedback.length > 0) { if (state.feedback && state.feedback.length > 0) {
...@@ -1372,7 +1389,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1372,7 +1389,7 @@ function DragAndDropBlock(runtime, element, configuration) {
} else { } else {
state.finished = true; state.finished = true;
} }
readScreenReaderMessages(); setScreenReaderMessages();
}).always(function() { }).always(function() {
state.submit_spinner = false; state.submit_spinner = false;
applyState(); applyState();
...@@ -1500,7 +1517,8 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1500,7 +1517,8 @@ function DragAndDropBlock(runtime, element, configuration) {
showing_answer: state.showing_answer, showing_answer: state.showing_answer,
show_answer_spinner: state.show_answer_spinner, show_answer_spinner: state.show_answer_spinner,
disable_go_to_beginning_button: !canGoToBeginning(), 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); return renderView(context);
......
...@@ -163,27 +163,6 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -163,27 +163,6 @@ class BaseIntegrationTest(SeleniumBaseTest):
focused_element = self.browser.switch_to.active_element focused_element = self.browser.switch_to.active_element
self.assertTrue(element != focused_element, 'expected element to not have focus') 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 @staticmethod
def get_element_html(element): def get_element_html(element):
return element.get_attribute('innerHTML').strip() return element.get_attribute('innerHTML').strip()
...@@ -510,3 +489,10 @@ class InteractionTestBase(object): ...@@ -510,3 +489,10 @@ class InteractionTestBase(object):
def assert_button_enabled(self, submit_button, enabled=True): def assert_button_enabled(self, submit_button, enabled=True):
self.assertEqual(submit_button.is_enabled(), enabled) 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): ...@@ -56,7 +56,6 @@ class ParameterizedTestsMixin(object):
if feedback is None: if feedback is None:
feedback = self.feedback feedback = self.feedback
get_sr_texts = self._patch_sr_read_text()
popup = self._get_popup() popup = self._get_popup()
feedback_popup_content = self._get_popup_content() feedback_popup_content = self._get_popup_content()
...@@ -79,7 +78,7 @@ class ParameterizedTestsMixin(object): ...@@ -79,7 +78,7 @@ class ParameterizedTestsMixin(object):
else: else:
overall_feedback = feedback['intro'] overall_feedback = feedback['intro']
expected_sr_texts.append(overall_feedback) 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: if action_key:
# Next TAB keypress should move focus to "Go to Beginning button" # Next TAB keypress should move focus to "Go to Beginning button"
self._test_next_tab_goes_to_go_to_beginning_button() self._test_next_tab_goes_to_go_to_beginning_button()
...@@ -90,7 +89,6 @@ class ParameterizedTestsMixin(object): ...@@ -90,7 +89,6 @@ class ParameterizedTestsMixin(object):
if feedback is None: if feedback is None:
feedback = self.feedback feedback = self.feedback
get_sr_texts = self._patch_sr_read_text()
popup = self._get_popup() popup = self._get_popup()
feedback_popup_content = self._get_popup_content() feedback_popup_content = self._get_popup_content()
...@@ -105,7 +103,7 @@ class ParameterizedTestsMixin(object): ...@@ -105,7 +103,7 @@ class ParameterizedTestsMixin(object):
feedback_popup_html = feedback_popup_content.get_attribute('innerHTML') feedback_popup_html = feedback_popup_content.get_attribute('innerHTML')
self.assertEqual(feedback_popup_html, '') self.assertEqual(feedback_popup_html, '')
self.assertFalse(popup.is_displayed()) self.assertFalse(popup.is_displayed())
self.assertEqual(get_sr_texts(), []) self.assert_reader_feedback_messages([])
def parameterized_item_negative_feedback_on_bad_move_standard( def parameterized_item_negative_feedback_on_bad_move_standard(
self, items_map, all_zones, scroll_down=100, action_key=None, feedback=None self, items_map, all_zones, scroll_down=100, action_key=None, feedback=None
...@@ -113,7 +111,6 @@ class ParameterizedTestsMixin(object): ...@@ -113,7 +111,6 @@ class ParameterizedTestsMixin(object):
if feedback is None: if feedback is None:
feedback = self.feedback feedback = self.feedback
get_sr_texts = self._patch_sr_read_text()
popup = self._get_popup() popup = self._get_popup()
feedback_popup_content = self._get_popup_content() feedback_popup_content = self._get_popup_content()
...@@ -129,7 +126,7 @@ class ParameterizedTestsMixin(object): ...@@ -129,7 +126,7 @@ class ParameterizedTestsMixin(object):
self.assertTrue(popup.is_displayed()) self.assertTrue(popup.is_displayed())
self.assert_reverted_item(definition.item_id) self.assert_reverted_item(definition.item_id)
expected_sr_texts = [definition.feedback_negative, feedback['intro']] 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) self._test_popup_focus_and_close(popup, action_key)
def parameterized_item_negative_feedback_on_bad_move_assessment( def parameterized_item_negative_feedback_on_bad_move_assessment(
...@@ -138,7 +135,6 @@ class ParameterizedTestsMixin(object): ...@@ -138,7 +135,6 @@ class ParameterizedTestsMixin(object):
if feedback is None: if feedback is None:
feedback = self.feedback feedback = self.feedback
get_sr_texts = self._patch_sr_read_text()
popup = self._get_popup() popup = self._get_popup()
feedback_popup_content = self._get_popup_content() feedback_popup_content = self._get_popup_content()
...@@ -154,7 +150,7 @@ class ParameterizedTestsMixin(object): ...@@ -154,7 +150,7 @@ class ParameterizedTestsMixin(object):
self.assertEqual(feedback_popup_html, '') self.assertEqual(feedback_popup_html, '')
self.assertFalse(popup.is_displayed()) self.assertFalse(popup.is_displayed())
self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True) 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) self._test_popup_focus_and_close(popup, action_key)
if action_key: if action_key:
self._test_next_tab_goes_to_go_to_beginning_button() self._test_next_tab_goes_to_go_to_beginning_button()
......
...@@ -248,8 +248,6 @@ class AssessmentInteractionTest( ...@@ -248,8 +248,6 @@ class AssessmentInteractionTest(
""" """
Test updating overall feedback after submitting solution in assessment mode 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): def check_feedback(overall_feedback_lines, per_item_feedback_lines=None):
# Check that the feedback is correctly displayed in the overall feedback area. # Check that the feedback is correctly displayed in the overall feedback area.
expected_overall_feedback = "\n".join(["FEEDBACK"] + overall_feedback_lines) expected_overall_feedback = "\n".join(["FEEDBACK"] + overall_feedback_lines)
...@@ -260,7 +258,7 @@ class AssessmentInteractionTest( ...@@ -260,7 +258,7 @@ class AssessmentInteractionTest(
if per_item_feedback_lines: if per_item_feedback_lines:
sr_feedback_lines += ["Some of your answers were not correct.", "Hints:"] sr_feedback_lines += ["Some of your answers were not correct.", "Hints:"]
sr_feedback_lines += per_item_feedback_lines 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 # 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) 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