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