Fwd: Accessibility: 'DATE accessibility' at groups.drupal.org

E.J. Zufelt everett at zufelt.ca
Fri Feb 13 06:01:24 UTC 2009


Good evening,

This message from the Drupal Accessibility group may be of interest to  
people working on the date picker.

HTH,
Everett



Begin forwarded message:

> From: wdmartin <NO-REPLY at groups.drupal.org>
> Date: February 13, 2009 1:30:17 AM AST
> To: everett at zufelt.ca
> Subject: Accessibility: 'DATE accessibility' at groups.drupal.org
> Reply-To: wdmartin <NO-REPLY at groups.drupal.org>
>
>
> wdmartin has posted a Discussion at http://groups.drupal.org/node/ 
> 19118
>
> DATE accessibility
> ---------------
> Getting Drupal to emit screen-reader friendly dates is a pain in the  
> butt.  It can, however, be done.  Here's a detailed account of my  
> efforts on that score, complete with tests in two screen readers  
> (including MP3s for your listening pleasure), some analysis, and a  
> detailed walk through of customizing the code for a DATE element.
> Let's look at an example.  Here's the PHP for a  fairly standard  
> date input in Drupal's Forms API, from a project I've been working on:
> <?php  $form['td']['rush']['rush-date'] = array(    '#type' =>  
> 'date',    '#title' => t('Needed by'),    '#required' => false,     
> '#default_value' => array(      'day' => format_date(time(),  
> 'custom', 'j'),      'month' => format_date(time(), 'custom',  
> 'n'),      'year' => format_date(time(), 'custom', 'Y'),    ),  );?>
> Drupal 6.9 renders that with the following HTML (reformatted and  
> abbreviated for legibility):
>   Needed by:                     1        2        3         
> [...]        31                            Jan        [...]         
> Dec                            1900        1901        1902         
> [...]        2050            I've put together a bare bones test  
> case. Here are some samples of how it's read by two screen readers:
>
> FireVox
> NVDA
>
> As the demographics of screen reader use make clear, the most common  
> screen reader out there is JAWS from Freedom Scientific.  However, I  
> do not have a purchased copy handy at this time, and the evaluation  
> version of JAWS does not permit its use in testing code.  Therefore  
> I will not be testing with it.
> There are some differences in how FireVox and NVDA read the sample.   
> You'll note that FireVox silently omits the "Needed by" label.  The  
> reason for that is in the code:
>   Needed by: The FOR attribute of this label associates it with the  
> input in the document which has the ID "edit-rush-date".   
> Unfortunately, there is no such input in the document.  I asked  
> Charles Chen, the developer of FireVox, about this a while back.  In  
> essence, he explained that he'd decided a LABEL element with no  
> associated form element shouldn't be read at all, because it would  
> erroneously lead the listener to believe that the form has more  
> inputs than it actually does.
> NVDA treats it differently -- it goes ahead and reads the orphaned  
> label.  Both NVDA and FireVox are affected by the other problem,  
> which is that the three individual inputs comprising the date picker  
> lack labels.  In the FireVox sample, you'll hear that it reads the  
> actual date pickers as:
>
> Select box: One
> Select box: Jan
> Select box: Nineteen hundred
>
> And NVDA reads it as:
>
> Needed By
> Combo box, collapsed submenu: One
> Combo box, collapsed submenu: Jan
> Combo box, collapsed submenu: Nineteen hundred
>
> In both cases it's not clear from the audible description what the  
> select boxes are actually for.  FireVox's omission of the label  
> makes it even less clear in that screen reader than in NVDA, but  
> neither one is very obvious.  It would help somewhat to have a more  
> recent year as the default value, of course, which didn't occur to  
> me till after I'd begun testing.
> But even then, the screen reader user is left to infer the purpose  
> of the form controls based on the types of information stored in  
> them -- a one or two-digit integer for the day, an abbreviated month  
> name for the month, and a four digit integer for the year.  The  
> order of the fields may get in the way of its understandability as  
> well, depending on your cultural background.  An American screen  
> reader user would probably expect dates inputs to be ordered Month- 
> Day-Year, while a British screen reader user would expect Day-Month- 
> Year.  In the absence of explicit labels for all three parts of the  
> date, blind visitors are left to guess, and the cultural difference  
> in date order just make it harder.
> The root of the problem is one of hierarchy.  A date picker like  
> this one is supposed to yield just one form value -- the date -- but  
> consists of three discrete form inputs for the day, month, and  
> year.  The discrete form inputs each require a label.  That part is  
> easy, like this:
>   Needed by:             Day:               1        2         
> 3        [...]        31                    Month:                
> Jan        [...]        Dec                    Year:                
> 1900        1901        1902        [...]        2050            The  
> labels can be hidden easily enough with CSS -- .container-inline  
> label { position: absolute; left: -999em; } would do it, though it  
> would be preferable to add a more specific class to the HTML like  
> class="form-item-date" to avoid applying the rule to other contexts.  
> Adding explicit labels this way makes it a lot easier to interpret  
> the purpose of the form controls, as you can hear:
>
> FireVox
> NVDA
>
> But in order to be fully intelligible, the group as a whole needs a  
> label too, and that label needs to be associated with all three form  
> elements.  It is possible to write code containing multiple labels  
> for a single form input:
>   Needed by:   Day:               1        [...]        31      And,  
> in fact, this is fully conformant with the HTML 4.01 standard, which  
> says that "More than one LABEL may be associated with the same  
> control by creating multiple references via the for  
> attribute."[ref]  The XHTML standard does not alter that.
> However, screen reader support for multiple labels is variable.   
> Given the above code, FireVox will speak both labels, but NVDA will  
> only speak the first one.
> Even if screen readers all supported multiple labels perfectly,  
> though using multiple labels wouldn't solve the problem.  The HTML  
> specification requires that each label be associated with exactly  
> one form control.  The FOR attribute takes an ID, not a class.  So  
> in order to label each of the three select boxes, we would need  
> three copies of the "Needed By" label, one for each of the three  
> form elements:
>   Needed by:   Needed by:   Needed by:   Day:               1         
> [...]        31              [...]
> That's obviously a bad solution, partly because it bloats the code,  
> but also because it requires us to hide two of those duplicate  
> labels from sighted visitors, which bloats the code even further.   
> Great.
> The natural HTML elements to use in this situation are FIELDSET and  
> LEGEND elements.  They were explicitly designed to group related  
> sets of form controls together and to provide a label for the whole  
> group in a screen-reader friendly way.  Something like this works  
> nicely:
>   Needed By:  Day:                 1        [...]         
> 31            Month:               Jan        [...]         
> Dec            Year:               1900        1901         
> [...]        2050        This is fully intelligible in both screen  
> readers:
>
> FireVox
> NVDA
>
> The next thing to take care of is its appearance.  The default  
> styling for a FIELDSET/LEGEND combo is much too visually complex for  
> just a date picker.  Fortunately, it's not terribly difficult to  
> make it look just like most other Drupal form items -- a bold label  
> one line above the input.  You can see a test case, using this CSS:
> /* Remove the visual apparatus of the fieldset. /  .form-date  
> {    border: 0;    margin: 0;    padding: 0;    background:  
> none;  }  / Hide the labels for the individual elements from sighted  
> visitors. /  .form-date label {    position: absolute;    left:  
> -999em;  }  / Hide the labels for the individual elements from  
> sighted visitors. /  .form-date legend { font-weight: bold; }   /  
> Add a little extra space between the legend and the parts, for  
> legibility. */  .form-date .inline-container { margin-top: 5px; }
> That renders acceptably in all four major browsers (screenshots).   
> All that's left is to make Drupal emit the correct HTML and CSS to  
> achieve this effect.  Now, from here on in I'm going to assume that  
> you're developing a module, because that's what I'm doing.  Some of  
> this can be replicated on the theme layer; other bits of it can't as  
> far as I know.  I'll indicate which is which.
> First up, let's get some labels in place for the individual elements  
> in the date.  The Drupal function responsible for that is  
> expand_date(), which takes the array of values provided in the  
> #default_value index of the form array that defines the structure of  
> the form.  All it really does is automatically create SELECT  
> elements in Drupal's Forms API notation.  To make it spit out an  
> appropriately formatted label, we'll need to override that function.
> The first step is to make a copy of the stock expand_date() function  
> into your module and rename it.  My module is named ds_ticket, so  
> the function will be ds_ticket_expand_date().  It actually only  
> needs a single line added to make it produce the labels -- I've  
> marked that with a comment below.  Thus:
> <?phpfunction ds_ticket_expand_date($element) {  // Default to  
> current date  if (empty($element['#value'])) {    $element['#value']  
> = array('day' => format_date(time(), 'custom',  
> 'j'),                            'month' => format_date(time(),  
> 'custom', 'n'),                            'year' =>  
> format_date(time(), 'custom', 'Y'));  }  $element['#tree'] =  
> TRUE;  // Determine the order of day, month, year in the site's  
> chosen date format.  $format = variable_get('date_format_short', 'm/ 
> d/Y - H:i');  $sort = array();  $sort['day'] = max(strpos($format,  
> 'd'), strpos($format, 'j'));  $sort['month'] = max(strpos($format,  
> 'm'), strpos($format, 'M'));  $sort['year'] = strpos($format, 'Y');   
> asort($sort);  $order = array_keys($sort);  // Output multi-selector  
> for date.  foreach ($order as $type) {    switch ($type) {      case  
> 'day':        $options = drupal_map_assoc(range(1, 31));        bre
> ak;      case 'month':        $options = drupal_map_assoc(range(1,  
> 12), 'map_month');        break;      case 'year':        $options =  
> drupal_map_assoc(range(1900, 2050));        break;    }    $parents  
> = $element['#parents'];    $parents[] = $type;    $element[$type] =  
> array(      '#type' => 'select',      '#title' => t($type), // <---  
> Add a label for each one.      '#value' => $element['#value'] 
> [$type],      '#attributes' => $element['#attributes'],       
> '#options' => $options,    );  }  return $element;}?>
> Once that's in place, we'll modify the original definition of the  
> form field to call our version of expand_date() instead of the stock  
> one.  Here's that form field definition again:
> <?php  $form['td']['rush']['rush-date'] = array(    '#type' =>  
> 'date',    '#title' => t('Needed by'),    '#required' => false,     
> '#default_value' => array(      'Day' => format_date(time(),  
> 'custom', 'j'),      'Month' => format_date(time(), 'custom',  
> 'n'),      'Year' => format_date(time(), 'custom', 'Y'),    ),     
> '#process' => array('ds_ticket_expand_date'), //  <----- Make it use  
> our custome function.  );?>
> Save the .module file, upload, and voila -- labels on all the  
> individual dates.  I don't know if you could accomplish the same  
> thing from a theme's template.php; I've never tried.  If it's  
> possible, it would probably require using hook_form_alter().
> Next up, we need to re-write the theming to make it generate the  
> fieldset and legend code.  The stock theming is handled by  
> theme_date().  We'll need to override that with a custom function.   
> The first step is to register a custom theming function using  
> hook_theme().  Like this:
> <?php/** * Implementation of hook_theme(); * Lists the theme  
> functions and templates used by the module, along with their  
> arguments (where applicable). */function ds_ticket_theme()  
> {  $themes = array();    // Theme functions for the form.    
> $themes['ds_ticket_date'] = array('arguments' => array()); return  
> $themes;}?>
> In this case, it's just a function, not a template file, and we  
> don't need to pass any unusual arguments to the new function.  The  
> new function itself must have the same name as the index in  
> hook_theme (here that's 'ds_ticket_date'), only with 'theme_' on the  
> front, so the full name of the function is 'theme_ds_ticket_date'.   
> If you're working from a template rather than a module, you can  
> override the stock theme_date() simply by creating a function in  
> your template.php file named either 'phptemplate_theme_date()' or  
> 'YourThemeNameHere_theme_date()'.  You may need to clear the Drupal  
> cache in order to make it recognize your new function, which can be  
> done by going to Administer --> Site Configuration --> Performance  
> and clicking the "Clear Cached Data" button.  If you're going to be  
> doing that a lot, though, you'll probably want to install the Devel  
> module, which adds a new block to your site with a very handy "Empty  
> Cache" link.
> In my module, I created the new function by copying and pasting the  
> stock function in and changing the function name, like this:
> <?phpfunction theme_ds_ticket_date($element) {  return  
> theme('form_element', $element, ''. $element['#children'] .'');}?>
> As you can see, it's a really basic function.  And, as yet, it's not  
> actually doing anything.  We've created the new theme function, and  
> registered its existence with Drupal, but we haven't specified that  
> the form element should use it yet.  That's done by adding a #theme  
> index, thus:
> <?php  $form['td']['rush']['rush-date'] = array(    '#type' =>  
> 'date',    '#title' => t('Needed by'),    '#required' => false,     
> '#default_value' => array(      'Day' => format_date(time(),  
> 'custom', 'j'),      'Month' => format_date(time(), 'custom',  
> 'n'),      'Year' => format_date(time(), 'custom', 'Y'),    ),     
> '#process' => array('ds_ticket_expand_date'),    '#theme' =>  
> array('ds_ticket_date'), // <--- Make it use our custom theme  
> function  );?>
> Now it should finally use our theme function.  Let's make it produce  
> a fieldset instead of the usual code.
> <?php function theme_ds_ticket_date($element) {  $title =  
> $element['#title'];  $fieldset = '';  $fieldset .= "$title";   
> $fieldset .= '';  // Build the day, month, and year select boxes.   
> $children = element_children($element);  foreach($children as $c) 
> { $fieldset .= drupal_render($element[$c]); }  $fieldset .= '';   
> $fieldset .= '';  return $fieldset;}?>
> This seems pretty straightforward, but it took a while to work out.   
> In the stock theme_date() function, it just threw the element at  
> theme() and passed it a value of $element['#children'] -- but when I  
> examined that in my own version, it didn't exist.  I believe  
> theme_date() actually gets called twice (or more) in the process of  
> building a date, but I'm not certain.  Because Drupal relies so  
> heavily on calling functions through call_user_func(), the execution  
> flow can be rather opaque.
> Anyway, I studied drupal_render() a bit.  Since  
> $element['#children'] didn't exist, I built it myself using  
> element_children() to retrieve the children, and drupal_render() to  
> render them as normal.
> Another problem I ran into was that Drupal invariably wrapped the  
> whole fieldset in another DIV containing a malformed LABEL.  It was  
> apparently calling theme_date() again after my own theme function  
> ran, and adding an extraneous wrapped.  I still don't know why it  
> was doing this, or the proper way of making it stop.  What  
> eventually fixed it for me was manually marking the form element as  
> "printed" in the initial form definition, thus:
> <?php  $form['td']['rush']['rush-date'] = array(    '#type' =>  
> 'date',    '#title' => t('Needed by'),    '#required' => false,     
> '#default_value' => array(      'Day' => format_date(time(),  
> 'custom', 'j'),      'Month' => format_date(time(), 'custom',  
> 'n'),      'Year' => format_date(time(), 'custom', 'Y'),    ),     
> '#value' => array(      'day' => format_date(time(), 'custom',  
> 'j'),      'month' => format_date(time(), 'custom', 'n'),       
> 'year' => format_date(time(), 'custom', 'Y'),    ),    '#process' =>  
> array('ds_ticket_expand_date'),    '#theme' =>  
> array('ds_ticket_date'),    '#printed' => true, // <--- Make it quit  
> calling theme_date() needlessly.  );?>
> And as you probably noticed, I also added a default value (today).   
> With that whipped into shape, there's nothing left to do but add the  
> CSS.  I had to explicitly add some space at the bottom of the form  
> element immediately above the date, because the LEGEND element in  
> the date was too close to its predecessor for easy legibility, and  
> adding margins or padding to the top of the fieldset did not help.   
> (LEGEND element are notoriously difficult to style -- they just  
> don't cooperate with a lot of ordinary style rules).
> And all of this fuss because the W3C didn't see fit to give us a  
> standard  or  back when they were last working on HTML, which would  
> have been a better way to do it.  That way the browser vendors could  
> have worried about it once, and then we poor long-suffering web  
> developers wouldn't have to.  I wonder how many hours of time have  
> been spent worrying about date/time inputs in forms?  Probably a  
> lot.  And now we're getting fancy JavaScript datepicker widgets  
> which are even more difficult to do in an accessible fashion.
> I hope this helps someone, as I've just spent nearly 8 hours working  
> out the code and documenting everything.
>
>
> --
> This is an automatic message from groups.drupal.org
> To manage your subscriptions, browse to http://groups.drupal.org/user/30025/notifications
> You can unsubscribe at http://groups.drupal.org/notifications/unsubscribe/11954?signature=ef5aa61c1faefbd68c150e7574ab29b6

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://fluidproject.org/pipermail/fluid-work/attachments/20090213/69e42aa6/attachment.html>


More information about the fluid-work mailing list