From 9de1bc0eef6f5bfaa2a8d745431caa361ae91548 Mon Sep 17 00:00:00 2001 From: kujiu Date: Fri, 22 Jun 2018 21:22:21 +0200 Subject: [IMP] Improve compatibility with screen readers (accessibility) (#24574) Today, Odoo is really tricky to use without seeing the screen, it must be improved to be usable. This PR forbid to use labels without a "for" attribute, add some title, rule and aria attributes in HTML. With that, Odoo will be fully usable with a screen reader. * [IMP] Labels must have a for attribute. Improve accessibility. * [IMP] Better error message when trying to read a missing cached value * [FIX] Add some aria-label and title attributes for screen readers. * [FIX] Template name is not included in the error message in case of SyntaxError in QWeb * [FIX] Improve the Tour failed at step error message to be more explicit. * [IMP] Add aria-labels * [FIX] Add missing aria-label on failing test * [IMP] aria-hidden means hidden. Fix all bad aria-hidden and hide aria-hidden for all. * [IMP] Color names on kanban views and many2many tags * [IMP] Add some checks on views for accessibility. * [IMP] Add `alt` attribute on `img` tags. * [IMP] Add aria-label and title on non-described icons * [IMP] Add button role to widgets with btn class * [IMP] Translate aria and formatted attributes. * [IMP] Remove wrong aria-labelledby * [IMP] Add menu role on dropdowns * [IMP] Buttons must be focusable * [IMP] Add aria attributes on progress bars * [IMP] Improve accessibility of basic widgets * [IMP] Change main layout to more semantic tags * [IMP] Add menuitem role when missing * [IMP] Remove wrong role='presentation' * [IMP] Improve accessibility of tab panels * [IMP] Add aria-invalid on invalid fields * [IMP] Add aria-sort on ordered columns * [IMP] Add role on alerts * [IMP] Use dialog role, header, main and footer tags for modals * [IMP] Add labels on o_status * [IMP] Improve accessibility of kanban view with feeds and articles * [IMP] Add alerts in case of new messages * [IMP] Add widget, navigation or img role to aria-labelled items --- odoo/tools/view_validation.py | 242 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 3 deletions(-) (limited to 'odoo/tools/view_validation.py') diff --git a/odoo/tools/view_validation.py b/odoo/tools/view_validation.py index 1ffdd43e58b..7c69906220d 100644 --- a/odoo/tools/view_validation.py +++ b/odoo/tools/view_validation.py @@ -15,9 +15,13 @@ _relaxng_cache = {} def valid_view(arch): for pred in _validators[arch.tag]: - if not pred(arch): + check = pred(arch) + if not check: _logger.error("Invalid XML: %s", pred.__doc__) return False + if check == "Warning": + _logger.warning("Invalid XML: %s", pred.__doc__) + return "Warning" return True @@ -88,8 +92,8 @@ def valid_att_in_field(arch): @validate('form') def valid_att_in_label(arch): - """ ``label`` nodes must have a ``@for`` or a ``@string`` """ - return not arch.xpath('//label[not(@for or @string)]') + """ ``label`` nodes must have a ``@for`` """ + return not arch.xpath('//label[not(@for) and not(descendant::input)]') @validate('form') @@ -113,3 +117,235 @@ def valid_type_in_col(arch): attrib.isdigit() for attrib in arch.xpath('//@col') ) + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_alternative_image_text(arch): + """An `img` tag must have an alt value.""" + if arch.xpath('//img[not(@alt or @t-att-alt or @t-attf-alt)]'): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_alternative_icon_text(arch): + """An icon with fa- class or in a button must have aria-label in its tag, parents, descendants or have text.""" + valid_aria_attrs = { + 'aria-label', 'aria-labelledby', 't-att-aria-label', 't-attf-aria-label', + 't-att-aria-labelledby', 't-attf-aria-labelledby'} + valid_t_attrs = {'t-value', 't-raw', 't-field', 't-esc'} + valid_attrs = valid_aria_attrs | valid_t_attrs + valid_aria_attrs_xpath = ' or '.join('@' + attr for attr in valid_aria_attrs) + valid_attrs_xpath = ' or '.join('@' + attr for attr in valid_attrs) + + # Select elements with class begining by 'fa-' + xpath = '(//*[contains(concat(" ", @class), " fa-")' + xpath += ' or contains(concat(" ", @t-att-class), " fa-")' + xpath += ' or contains(concat(" ", @t-attf-class), " fa-")]' + xpath += ' | //button[@icon])' + # Elements with accessibility or string attrs are good + xpath += '[not(' + valid_attrs_xpath + ')]' + # And we ignore all elements with describing in children + xpath += '[not(//*[' + valid_attrs_xpath + '])]' + # Aria label can be on ancestors + xpath += '[not(ancestor[' + valid_aria_attrs_xpath + '])]' + # Labels provide text by definition + xpath += '[not(descendant-or-self::label)]' + # Buttons can have a string attribute + xpath += '[not(descendant-or-self::button[@string])]' + # Fields have labels + xpath += '[not(descendant-or-self::field)]' + # And finally, if there is some text, it's good too + xpath += '[not(descendant-or-self::*[text()])]' + + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_title_icon(arch): + """An icon with fa- class or in a button must have title in its tag, parents, descendants or have text.""" + valid_title_attrs = {'title', 't-att-title', 't-attf-title'} + valid_t_attrs = {'t-value', 't-raw', 't-field', 't-esc'} + valid_attrs = valid_title_attrs | valid_t_attrs + valid_title_attrs_xpath = ' or '.join('@' + attr for attr in valid_title_attrs) + valid_attrs_xpath = ' or '.join('@' + attr for attr in valid_attrs) + + # Select elements with class begining by 'fa-' + xpath = '(//*[contains(concat(" ", @class), " fa-")' + xpath += ' or contains(concat(" ", @t-att-class), " fa-")' + xpath += ' or contains(concat(" ", @t-attf-class), " fa-")]' + xpath += ' | //button[@icon])' + # Elements with accessibility or string attrs are good + xpath += '[not(' + valid_attrs_xpath + ')]' + # And we ignore all elements with describing in children + xpath += '[not(//*[' + valid_attrs_xpath + '])]' + # Aria label can be on ancestors + xpath += '[not(ancestor[' + valid_title_attrs_xpath + '])]' + # Labels provide text by definition + xpath += '[not(descendant-or-self::label)]' + # Buttons can have a string attribute + xpath += '[not(descendant-or-self::button[@string])]' + # Fields have labels + xpath += '[not(descendant-or-self::field)]' + # And finally, if there is some text, it's good too + xpath += '[not(descendant-or-self::*[text()])]' + + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_simili_button(arch): + """A simili button must be tagged with "role='button'".""" + # Select elements with class 'btn' + xpath = '//a[contains(concat(" ", @class), " btn")' + xpath += ' or contains(concat(" ", @t-att-class), " btn")' + xpath += ' or contains(concat(" ", @t-attf-class), " btn")]' + xpath += '[not(@role="button")]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_simili_dropdown(arch): + """A simili dropdown must be tagged with "role='menu'".""" + xpath = '//*[contains(concat(" ", @class, " "), " dropdown-menu ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " dropdown-menu ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " dropdown-menu ")]' + xpath += '[not(@role="menu")]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_simili_progressbar(arch): + """A simili progressbar must be tagged with "role='progressbar'" and have + aria-valuenow, aria-valuemin and aria-valuemax attributes.""" + # Select elements with class 'btn' + xpath = '//*[contains(concat(" ", @class, " "), " o_progressbar ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " o_progressbar ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " o_progressbar ")]' + xpath += '[not(self::progress)]' + xpath += '[not(@role="progressbar")]' + xpath += '[not(@aria-valuenow or @t-att-aria-valuenow or @t-attf-aria-valuenow)]' + xpath += '[not(@aria-valuemin or @t-att-aria-valuemin or @t-attf-aria-valuemin)]' + xpath += '[not(@aria-valuemax or @t-att-aria-valuemax or @t-attf-aria-valuemax)]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_dialog(arch): + """A dialog must use role="dialog" and its header, body and footer contents must use
,
and