Commit c773d9fa by Matjaz Gregoric Committed by GitHub

Merge pull request #111 from open-craft/mtyaka/usability-improvements

Usability improvements for screen reader users
parents 8cb4774d d087caf3
...@@ -671,15 +671,37 @@ ...@@ -671,15 +671,37 @@
padding: 7px; padding: 7px;
background-color: #e5e5e5; background-color: #e5e5e5;
text-align: left; text-align: left;
direction: ltr;
z-index: 1500; z-index: 1500;
} }
.rtl .xblock--drag-and-drop .modal-window {
transform: translate(50%, -50%);
}
.xblock--drag-and-drop .modal-dismiss-button {
font-size: 24px;
position: absolute;
top: 3px;
right: 3px;
padding: 5px 8px;
}
.rtl .xblock--drag-and-drop .modal-dismiss-button {
right: inherit;
left: 3px;
}
.xblock--drag-and-drop .modal-header h2 {
height: 30px;
line-height: 30px;
margin-bottom: 5px;
}
.xblock--drag-and-drop .modal-content { .xblock--drag-and-drop .modal-content {
border-radius: 5px; border-radius: 5px;
background-color: #ffffff; background-color: #ffffff;
margin-bottom: 5px; padding: 8px;
padding: 5px;
} }
.xblock--drag-and-drop .modal-content li { .xblock--drag-and-drop .modal-content li {
......
...@@ -527,7 +527,11 @@ msgid "" ...@@ -527,7 +527,11 @@ msgid ""
msgstr "" msgstr ""
#: public/js/drag_and_drop.js #: public/js/drag_and_drop.js
msgid "OK" msgid "Close"
msgstr ""
#: public/js/drag_and_drop.js
msgid "Go to Beginning"
msgstr "" msgstr ""
#: public/js/drag_and_drop.js #: public/js/drag_and_drop.js
......
...@@ -627,8 +627,12 @@ msgstr "" ...@@ -627,8 +627,12 @@ msgstr ""
"ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂ є#" "ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂ є#"
#: public/js/drag_and_drop.js #: public/js/drag_and_drop.js
msgid "OK" msgid "Close"
msgstr "ÖK Ⱡ'σя#" msgstr "Çlösé Ⱡ'σя#"
#: public/js/drag_and_drop.js
msgid "Go to Beginning"
msgstr "Gö tö Bégïnnïng Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#"
#: public/js/drag_and_drop.js #: public/js/drag_and_drop.js
msgid "Problem" msgid "Problem"
......
...@@ -124,6 +124,9 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -124,6 +124,9 @@ class BaseIntegrationTest(SeleniumBaseTest):
def _get_keyboard_help_dialog(self): def _get_keyboard_help_dialog(self):
return self._page.find_element_by_css_selector(".keyboard-help-dialog") return self._page.find_element_by_css_selector(".keyboard-help-dialog")
def _get_go_to_beginning_button(self):
return self._page.find_element_by_css_selector('.go-to-beginning-button')
def _get_reset_button(self): def _get_reset_button(self):
return self._page.find_element_by_css_selector('.reset-button') return self._page.find_element_by_css_selector('.reset-button')
...@@ -152,6 +155,14 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -152,6 +155,14 @@ class BaseIntegrationTest(SeleniumBaseTest):
query = 'return $("{selector}").get(0).style.{style}' query = 'return $("{selector}").get(0).style.{style}'
return self.browser.execute_script(query.format(selector=selector, style=style)) return self.browser.execute_script(query.format(selector=selector, style=style))
def assertFocused(self, element):
focused_element = self.browser.switch_to.active_element
self.assertTrue(element == focused_element, 'expected element to have focus')
def assertNotFocused(self, element):
focused_element = self.browser.switch_to.active_element
self.assertTrue(element != focused_element, 'expected element to not have focus')
@staticmethod @staticmethod
def get_element_html(element): def get_element_html(element):
return element.get_attribute('innerHTML').strip() return element.get_attribute('innerHTML').strip()
......
...@@ -63,7 +63,7 @@ class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, BaseEventsT ...@@ -63,7 +63,7 @@ class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, BaseEventsT
{ {
'name': 'edx.drag_and_drop_v2.feedback.closed', 'name': 'edx.drag_and_drop_v2.feedback.closed',
'data': { 'data': {
'manually': False, 'manually': True,
'content': ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE), 'content': ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE),
'truncated': False, 'truncated': False,
}, },
......
...@@ -27,6 +27,29 @@ ITEM_DRAG_KEYBOARD_KEYS = (None, Keys.RETURN, Keys.CONTROL+'m') ...@@ -27,6 +27,29 @@ ITEM_DRAG_KEYBOARD_KEYS = (None, Keys.RETURN, Keys.CONTROL+'m')
class ParameterizedTestsMixin(object): class ParameterizedTestsMixin(object):
def _test_popup_focus_and_close(self, popup, action_key):
dismiss_popup_button = popup.find_element_by_css_selector('.close-feedback-popup-button')
self.assertFocused(dismiss_popup_button)
# Assert focus is trapped - trying to tab out of the popup does not work, focus remains on the close button.
ActionChains(self.browser).send_keys(Keys.TAB).perform()
self.assertFocused(dismiss_popup_button)
# Close the popup now.
if action_key:
ActionChains(self.browser).send_keys(Keys.RETURN).perform()
else:
dismiss_popup_button.click()
self.assertFalse(popup.is_displayed())
# Assert focus moves to first enabled button in item bank after closing the popup.
focusable_items_in_bank = [item for item in self._get_items() if item.get_attribute('tabindex') == '0']
if len(focusable_items_in_bank) > 0:
self.assertFocused(focusable_items_in_bank[0])
def _test_next_tab_goes_to_go_to_beginning_button(self):
go_to_beginning_button = self._get_go_to_beginning_button()
self.assertNotFocused(go_to_beginning_button)
ActionChains(self.browser).send_keys(Keys.TAB).perform()
self.assertFocused(go_to_beginning_button)
def parameterized_item_positive_feedback_on_good_move( def parameterized_item_positive_feedback_on_good_move(
self, items_map, scroll_down=100, action_key=None, assessment_mode=False self, items_map, scroll_down=100, action_key=None, assessment_mode=False
): ):
...@@ -44,10 +67,14 @@ class ParameterizedTestsMixin(object): ...@@ -44,10 +67,14 @@ class ParameterizedTestsMixin(object):
if assessment_mode: if assessment_mode:
self.assertEqual(feedback_popup_html, '') self.assertEqual(feedback_popup_html, '')
self.assertFalse(popup.is_displayed()) self.assertFalse(popup.is_displayed())
if action_key:
# Next TAB keypress should move focus to "Go to Beginning button"
self._test_next_tab_goes_to_go_to_beginning_button()
else: else:
self.assertEqual(feedback_popup_html, "<p>{}</p>".format(definition.feedback_positive)) self.assertEqual(feedback_popup_html, "<p>{}</p>".format(definition.feedback_positive))
self.assert_popup_correct(popup) self.assert_popup_correct(popup)
self.assertTrue(popup.is_displayed()) self.assertTrue(popup.is_displayed())
self._test_popup_focus_and_close(popup, action_key)
def parameterized_item_negative_feedback_on_bad_move( def parameterized_item_negative_feedback_on_bad_move(
self, items_map, all_zones, scroll_down=100, action_key=None, assessment_mode=False self, items_map, all_zones, scroll_down=100, action_key=None, assessment_mode=False
...@@ -75,11 +102,14 @@ class ParameterizedTestsMixin(object): ...@@ -75,11 +102,14 @@ 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)
if action_key:
self._test_next_tab_goes_to_go_to_beginning_button()
else: else:
self.wait_until_html_in(definition.feedback_negative, feedback_popup_content) self.wait_until_html_in(definition.feedback_negative, feedback_popup_content)
self.assert_popup_incorrect(popup) self.assert_popup_incorrect(popup)
self.assertTrue(popup.is_displayed()) self.assertTrue(popup.is_displayed())
self.assert_reverted_item(definition.item_id) self.assert_reverted_item(definition.item_id)
self._test_popup_focus_and_close(popup, action_key)
def parameterized_move_items_between_zones(self, items_map, all_zones, scroll_down=100, action_key=None): def parameterized_move_items_between_zones(self, items_map, all_zones, scroll_down=100, action_key=None):
# Scroll drop zones into view to make sure Selenium can successfully drop items # Scroll drop zones into view to make sure Selenium can successfully drop items
...@@ -90,6 +120,8 @@ class ParameterizedTestsMixin(object): ...@@ -90,6 +120,8 @@ class ParameterizedTestsMixin(object):
for zone_id, zone_title in all_zones: for zone_id, zone_title in all_zones:
self.place_item(item_key, zone_id, action_key) self.place_item(item_key, zone_id, action_key)
self.assert_placed_item(item_key, zone_title, assessment_mode=True) self.assert_placed_item(item_key, zone_title, assessment_mode=True)
if action_key:
self._test_next_tab_goes_to_go_to_beginning_button()
# Finally, move them all back to the bank. # Finally, move them all back to the bank.
self.place_item(item_key, None, action_key) self.place_item(item_key, None, action_key)
self.assert_reverted_item(item_key) self.assert_reverted_item(item_key)
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from ddt import ddt, unpack, data from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.keys import Keys
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
...@@ -210,6 +211,33 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -210,6 +211,33 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.assertEqual(popup_content.text, "") self.assertEqual(popup_content.text, "")
self.assertEqual(popup_wrapper.get_attribute('aria-live'), 'polite') self.assertEqual(popup_wrapper.get_attribute('aria-live'), 'polite')
@data(None, Keys.RETURN)
def test_go_to_beginning_button(self, action_key):
self.load_scenario()
self.scroll_down(250)
button = self._get_go_to_beginning_button()
# Button is only visible to screen reader users by default.
self.assertIn('sr', button.get_attribute('class').split())
# Set focus to the element. We have to use execute_script here because while TAB-ing
# to the button to make it the active element works in selenium, the focus event is not
# emitted unless the Firefox window controlled by selenium is the focused window, which
# usually is not the case when running integration tests.
# See: https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/7346
self.browser.execute_script('$("button.go-to-beginning-button").focus()')
self.assertFocused(button)
# Button should be visible when focused.
self.assertNotIn('sr', button.get_attribute('class').split())
# Click/activate the button to move focus to the top.
if action_key:
button.send_keys(action_key)
else:
button.click()
first_focusable_item = self._get_items()[0]
self.assertFocused(first_focusable_item)
# Button should only be visible to screen readers again.
self.assertIn('sr', button.get_attribute('class').split())
def test_keyboard_help(self): def test_keyboard_help(self):
self.load_scenario() self.load_scenario()
......
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