Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1254 diff -u -p -r1.1254 common.inc --- includes/common.inc 14 Nov 2010 21:04:45 -0000 1.1254 +++ includes/common.inc 16 Nov 2010 13:45:30 -0000 @@ -6424,6 +6424,9 @@ function drupal_common_theme() { 'form_element_label' => array( 'render element' => 'element', ), + 'form_error_marker' => array( + 'render element' => 'element', + ), 'vertical_tabs' => array( 'render element' => 'element', ), Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.510 diff -u -p -r1.510 form.inc --- includes/form.inc 15 Nov 2010 10:06:32 -0000 1.510 +++ includes/form.inc 16 Nov 2010 13:45:35 -0000 @@ -1252,7 +1252,16 @@ function _form_validate(&$elements, &$fo // Instead of setting no #title, form constructors are encouraged to set // #title_display to 'invisible' to improve accessibility. if (isset($elements['#title'])) { - form_error($elements, $t('!name field is required.', array('!name' => $elements['#title']))); + if (isset($elements['#attributes']['id'])) { + $id = $elements['#attributes']['id']; + } + else { + $id = $elements['#id']; + } + form_error($elements, $t('!title field is required.', array( + '@field_id' => $id, + '!title' => $elements['#title'], + ))); } else { form_error($elements); @@ -1492,6 +1501,9 @@ function form_get_errors() { function form_get_error($element) { $form = form_set_error(); $parents = array(); + if (!isset($element['#parents'])) { + return; + } foreach ($element['#parents'] as $parent) { $parents[] = $parent; $key = implode('][', $parents); @@ -3847,6 +3859,11 @@ function theme_form_element_label($varia // If the element is required, a required marker is appended to the label. $required = !empty($element['#required']) ? theme('form_required_marker', array('element' => $element)) : ''; + // If the element has an error, append an error marker to the label. By + // default we append the error message text styled as invisible. This allows + // screen reader users to know which field has an error and what the error is. + $error = theme('form_error_marker', array('element' => $element)); + $title = filter_xss_admin($element['#title']); $attributes = array(); @@ -3864,7 +3881,50 @@ function theme_form_element_label($varia } // The leading whitespace helps visually separate fields from inline labels. - return ' ' . $t('!title !required', array('!title' => $title, '!required' => $required)) . "\n"; + return ' ' . $t('!title !required !error', array('!title' => $title, '!required' => $required, '!error' => $error)) . "\n"; +} + +/** + * Theme an invisible error marker within the label for form elements. + * + * For each form element with an error, this function outputs an error marker + * that is appended to the label next to each field. By default this outputs + * the error message text in an invisible span. This allows screen reader users + * to know which fields have an error and what the error is. Override this + * function if you want to provide a different error marker, such as visible + * error text or an icon with a tooltip. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * + * @return + * A string with a marker to identify an error, otherwise an empty string. + * + * @ingroup themeable + */ +function theme_form_error_marker($variables) { + $element = $variables['element']; + // This is also used in the installer, pre-database setup. + $t = get_t(); + + $output = ''; + if ($raw_error = form_get_error($element)) { + // This is the same test used to validate #required fields. + // @see _form_validate() + if ($element['#required'] && (!count($element['#value']) || (is_string($element['#value']) && strlen(trim($element['#value'])) == 0) || $element['#value'] === 0)) { + $error = $t('This field is required.'); + } + else { + $error = strip_tags($raw_error); + } + $attributes = array( + 'class' => array('element-invisible', 'error'), + ); + $output .= ' ' . $error . ''; + } + + return $output; } /** Index: modules/simpletest/tests/form.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v retrieving revision 1.75 diff -u -p -r1.75 form.test --- modules/simpletest/tests/form.test 15 Nov 2010 10:06:32 -0000 1.75 +++ modules/simpletest/tests/form.test 16 Nov 2010 13:45:39 -0000 @@ -66,7 +66,7 @@ class FormsTestCase extends DrupalWebTes $elements['file']['empty_values'] = $empty_strings; // Regular expression to find the expected marker on required elements. - $required_marker_preg = '@\*@'; + $required_marker_preg = '@\*.*@'; // Go through all the elements and all the empty values for them. foreach ($elements as $type => $data) { @@ -131,7 +131,7 @@ class FormsTestCase extends DrupalWebTes // First, try to submit without the required checkbox. $edit = array(); $this->drupalPost('form-test/checkbox', $edit, t('Submit')); - $this->assertRaw(t('!name field is required.', array('!name' => 'required_checkbox')), t('A required checkbox is actually mandatory')); + $this->assertText(t('!title field is required.', array('!title' => 'required_checkbox')), t('A required checkbox is actually mandatory')); // Now try to submit the form correctly. $values = drupal_json_decode($this->drupalPost(NULL, array('required_checkbox' => 1), t('Submit'))); @@ -158,26 +158,26 @@ class FormsTestCase extends DrupalWebTes function testSelect() { $form = $form_state = array(); $form = form_test_select($form, $form_state); - $error = '!name field is required.'; + $error = '!title field is required.'; $this->drupalGet('form-test/select'); // Posting without any values should throw validation errors. $this->drupalPost(NULL, array(), 'Submit'); - $this->assertNoText(t($error, array('!name' => $form['select']['#title']))); - $this->assertNoText(t($error, array('!name' => $form['select_required']['#title']))); - $this->assertNoText(t($error, array('!name' => $form['select_optional']['#title']))); - $this->assertNoText(t($error, array('!name' => $form['empty_value']['#title']))); - $this->assertNoText(t($error, array('!name' => $form['empty_value_one']['#title']))); - $this->assertText(t($error, array('!name' => $form['no_default']['#title']))); - $this->assertNoText(t($error, array('!name' => $form['no_default_optional']['#title']))); - $this->assertText(t($error, array('!name' => $form['no_default_empty_option']['#title']))); - $this->assertNoText(t($error, array('!name' => $form['no_default_empty_option_optional']['#title']))); - $this->assertText(t($error, array('!name' => $form['no_default_empty_value']['#title']))); - $this->assertText(t($error, array('!name' => $form['no_default_empty_value_one']['#title']))); - $this->assertNoText(t($error, array('!name' => $form['no_default_empty_value_optional']['#title']))); - $this->assertNoText(t($error, array('!name' => $form['multiple']['#title']))); - $this->assertNoText(t($error, array('!name' => $form['multiple_no_default']['#title']))); - $this->assertText(t($error, array('!name' => $form['multiple_no_default_required']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['select']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['select_required']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['select_optional']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['empty_value']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['empty_value_one']['#title']))); + $this->assertText(t($error, array('!title' => $form['no_default']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['no_default_optional']['#title']))); + $this->assertText(t($error, array('!title' => $form['no_default_empty_option']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['no_default_empty_option_optional']['#title']))); + $this->assertText(t($error, array('!title' => $form['no_default_empty_value']['#title']))); + $this->assertText(t($error, array('!title' => $form['no_default_empty_value_one']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['no_default_empty_value_optional']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['multiple']['#title']))); + $this->assertNoText(t($error, array('!title' => $form['multiple_no_default']['#title']))); + $this->assertText(t($error, array('!title' => $form['multiple_no_default_required']['#title']))); // Post values for required fields. $edit = array( @@ -533,7 +533,7 @@ class FormValidationTestCase extends Dru // validated, but the #element_validate handler for the 'test' field // is triggered. $this->drupalPost($path, $edit, t('Partial validate')); - $this->assertNoText(t('!name field is required.', array('!name' => 'Title'))); + $this->assertNoText(t('!title field is required.', array('!title' => 'Title'))); $this->assertText('Test element is invalid'); // Edge case of #limit_validation_errors containing numeric indexes: same @@ -555,7 +555,7 @@ class FormValidationTestCase extends Dru // Now test full form validation and ensure that the #element_validate // handler is still triggered. $this->drupalPost($path, $edit, t('Full validate')); - $this->assertText(t('!name field is required.', array('!name' => 'Title'))); + $this->assertText(t('!title field is required.', array('!title' => 'Title'))); $this->assertText('Test element is invalid'); } } @@ -972,7 +972,7 @@ class FormsFormStorageTestCase extends D // Test it again, but first trigger a validation error, then test. $this->drupalPost('form-test/state-persist', array('title' => ''), t('Submit'), $options); - $this->assertText(t('!name field is required.', array('!name' => 'title'))); + $this->assertText(t('!title field is required.', array('!title' => 'title'))); // Submit the form again triggering no validation error. $this->drupalPost(NULL, array('title' => 'foo'), t('Submit'), $options); $this->assertText('State persisted.');