Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1232 diff -u -p -r1.1232 common.inc --- includes/common.inc 4 Oct 2010 17:46:00 -0000 1.1232 +++ includes/common.inc 5 Oct 2010 18:51:58 -0000 @@ -6058,6 +6058,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.500 diff -u -p -r1.500 form.inc --- includes/form.inc 4 Oct 2010 18:00:45 -0000 1.500 +++ includes/form.inc 5 Oct 2010 18:51:58 -0000 @@ -1231,7 +1231,10 @@ 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']))); + form_error($elements, $t('!title field is required.', array( + '!field_id' => $elements['#id'], + '!title' => $elements['#title'], + ))); } else { form_error($elements); @@ -1469,6 +1472,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); @@ -3589,6 +3595,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(); @@ -3606,7 +3617,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/field/tests/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/tests/field.test,v retrieving revision 1.42 diff -u -p -r1.42 field.test --- modules/field/tests/field.test 28 Sep 2010 02:30:31 -0000 1.42 +++ modules/field/tests/field.test 5 Oct 2010 18:51:58 -0000 @@ -1300,7 +1300,7 @@ class FieldFormTestCase extends FieldTes // Submit with missing required value. $edit = array(); $this->drupalPost('test-entity/add/test-bundle', $edit, t('Save')); - $this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation'); + $this->assertText(t('!title field is required.', array('!title' => $this->instance['label'])), 'Required field with no value fails validation'); // Create an entity $value = mt_rand(1, 127); @@ -1316,7 +1316,7 @@ class FieldFormTestCase extends FieldTes $value = ''; $edit = array("{$this->field_name}[$langcode][0][value]" => $value); $this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save')); - $this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation'); + $this->assertText(t('!title field is required.', array('!title' => $this->instance['label'])), 'Required field with no value fails validation'); } // function testFieldFormMultiple() { Index: modules/simpletest/tests/form.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v retrieving revision 1.72 diff -u -p -r1.72 form.test --- modules/simpletest/tests/form.test 4 Oct 2010 18:00:46 -0000 1.72 +++ modules/simpletest/tests/form.test 5 Oct 2010 18:51:58 -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( @@ -468,7 +468,7 @@ class FormValidationTestCase extends Dru // ensure that the title field is not 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'); // Ensure not validated values are not available to submit handlers. @@ -478,7 +478,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'); } } @@ -895,7 +895,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.');