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