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 <[email protected]>
Date: February 13, 2009 1:30:17 AM AST
To: [email protected]
Subject: Accessibility: 'DATE accessibility' at groups.drupal.org
Reply-To: wdmartin <[email protected]>


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

_______________________________________________________
fluid-work mailing list - [email protected]
To unsubscribe, change settings or access archives,
see http://fluidproject.org/mailman/listinfo/fluid-work

Reply via email to