Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.981 diff -u -p -d -p -r1.981 common.inc --- includes/common.inc 31 Aug 2009 18:43:12 -0000 1.981 +++ includes/common.inc 3 Sep 2009 00:44:32 -0000 @@ -4640,6 +4640,9 @@ function drupal_common_theme() { 'form_element' => array( 'arguments' => array('element' => NULL), ), + 'form_element_label' => array( + 'arguments' => array('element' => NULL), + ), 'text_format_wrapper' => array( 'arguments' => array('element' => NULL), ), Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.368 diff -u -p -d -p -r1.368 form.inc --- includes/form.inc 29 Aug 2009 16:30:14 -0000 1.368 +++ includes/form.inc 3 Sep 2009 00:44:32 -0000 @@ -1082,6 +1082,11 @@ function form_builder($form_id, $element $form_state['has_file_element'] = TRUE; } + // Set the element's title attribute to the elements #title, if needed. + if (!empty($element['#title']) && isset($element['#show_title']) && $element['#show_title'] == FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE) { + $element['#attributes']['title'] = $element['#title']; + } + if (isset($element['#type']) && $element['#type'] == 'form') { // We are on the top form. // If there is a file element, we set the form encoding. @@ -1510,7 +1515,6 @@ function form_options_flatten($array, $r * values are associative arrays in the normal $options format. */ function theme_select($element) { - $select = ''; $size = $element['#size'] ? ' size="' . $element['#size'] . '"' : ''; _form_set_class($element, array('form-select')); $multiple = $element['#multiple']; @@ -1660,9 +1664,6 @@ function theme_radio($element) { $output .= 'value="' . $element['#return_value'] . '" '; $output .= (check_plain($element['#value']) == $element['#return_value']) ? ' checked="checked" ' : ' '; $output .= drupal_attributes($element['#attributes']) . ' />'; - if (!is_null($element['#title'])) { - $output = ''; - } return $output; } @@ -1998,10 +1999,6 @@ function theme_checkbox($element) { $checkbox .= $element['#value'] ? ' checked="checked" ' : ' '; $checkbox .= drupal_attributes($element['#attributes']) . ' />'; - if (!is_null($element['#title'])) { - $checkbox = ''; - } - return $checkbox; } @@ -2562,7 +2559,8 @@ function theme_file($element) { * * @param element * An associative array containing the properties of the element. - * Properties used: #title, #description, #id, #required, #children + * Properties used: #type, #name, #title, #show_title, #children, + * #description * @return * A string representing the form element. * @@ -2582,19 +2580,19 @@ function theme_form_element($element) { } $output = '
' . "\n"; - $required = !empty($element['#required']) ? '*' : ''; - if (!empty($element['#title']) && empty($element['#form_element_skip_title'])) { - $title = $element['#title']; - if (!empty($element['#id'])) { - $output .= ' \n"; - } - else { - $output .= ' \n"; - } + if (isset($element['#show_title']) && $element['#show_title'] == FORM_ELEMENT_SHOW_TITLE_AFTER) { + // Insert label in correct position, depending on #show_title. + $output .= $element['#children'] . ' ' . theme('form_element_label', $element) . "\n"; + } + else if (isset($element['#show_title']) && $element['#show_title'] == FALSE) { + // #show_title is FALSE, so we continue with no label (not recommended). + $output .= ' ' . $element['#children'] . "\n"; + } + else { + // Default (label before element) or FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE. + $output .= theme('form_element_label', $element) . ' ' . $element['#children'] . "\n"; } - - $output .= " " . $element['#children'] . "\n"; if (!empty($element['#description'])) { $output .= '
' . $element['#description'] . "
\n"; @@ -2606,6 +2604,44 @@ function theme_form_element($element) { } /** + * Theme a form element label and required mark. + * + * @param element + * An associative array containing the properties of the element. + * Properties used: #required, #show_title, #title, #label_option, #id + * @return + * A string representing the form element label and/or required mark. + * + * @ingroup themeable + */ +function theme_form_element_label($element) { + // This is also used in the installer, pre-database setup. + $t = get_t(); + + $required = !empty($element['#required']) ? '*' : ''; + // If the title is in an attribute we simply return any required mark here. + if (isset($element['#show_title']) && $element['#show_title'] == FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE) { + return $required; + } + + $title = $element['#title']; + // We don't want to return an empty label tag. + if (!empty($title)) { + $attributes = array(); + if (isset($element['#label_option']) && $element['#label_option']) { + // This class styles the label as an option, inline with the element. + $attributes['class'] = 'option'; + } + if (!empty($element['#id'])) { + $attributes['for'] = $element['#id']; + } + return ' ' . $t('!title !required', array('!title' => filter_xss_admin($title), '!required' => $required)) . "\n"; + } + // The element has no title or label, so return the required mark, if any. + return $required; +} + +/** * Sets a form element's class attribute. * * Adds 'required' and 'error' classes as needed. Index: modules/simpletest/tests/form.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v retrieving revision 1.14 diff -u -p -d -p -r1.14 form.test --- modules/simpletest/tests/form.test 13 Jul 2009 21:51:41 -0000 1.14 +++ modules/simpletest/tests/form.test 3 Sep 2009 00:44:32 -0000 @@ -112,6 +112,58 @@ class FormsTestTypeCase extends DrupalUn } /** + * Test the form elements and labels for expected behavior. + */ +class FormsElementsLabelsTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Form element and label output test', + 'description' => 'Test the form elements and labels for expected behavior', + 'group' => 'Form API', + ); + } + + function setUp() { + parent::setUp('form_test'); + } + + /** + * Test form elements, labels and title attibutes output correctly and have + * the correct label option class if needed. + */ + function testFormLabels() { + $this->drupalGet('form_test/form-labels'); + + $elements = $this->xpath('//input[@id="edit-form-checkboxes-test-thirdcheckbox"]/following-sibling::label[@for="edit-form-checkboxes-test-thirdcheckbox" and @class="option"]'); + $this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for regular checkboxes.")); + + $elements = $this->xpath('//input[@id="edit-form-radios-test-secondradio"]/following-sibling::label[@for="edit-form-radios-test-secondradio" and @class="option"]'); + $this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for regular radios.")); + + $elements = $this->xpath('//input[@id="edit-form-checkbox-test"]/following-sibling::label[@for="edit-form-checkbox-test" and @class="option"]'); + $this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for a checkbox.")); + + $elements = $this->xpath('//input[@id="edit-form-checkbox-test-attribute" and @title="Checkbox test with label as attribute"]'); + $this->assertTrue(isset($elements[0]), t("Title as attribute for a checkbox is correct.")); + + $elements = $this->xpath('//input[@id="edit-form-checkbox-test-attribute"]/following-sibling::label'); + $this->assertFalse(isset($elements[0]), t("Label does not follow title as attribute for a checkbox.")); + + $elements = $this->xpath('//input[@id="edit-form-checkbox-test-no-label-option"]/following-sibling::label[@for="edit-form-checkbox-test-no-label-option"]'); + $this->assertTrue(isset($elements[0]), t("Label follows field for a checkbox without a label option class.")); + + $elements = $this->xpath('//input[@id="edit-form-checkbox-test-no-label-option"]/following-sibling::label[@for="edit-form-checkbox-test-no-label-option" and @class="option"]'); + $this->assertFalse(isset($elements[0]), t("No label option class found for checkbox without a label option class.")); + + $elements = $this->xpath('//label[@for="edit-form-textfield-test-title-and-required"]/child::span[@class="form-required"]/parent::*/following-sibling::input[@id="edit-form-textfield-test-title-and-required"]'); + $this->assertTrue(isset($elements[0]), t("Label preceeds textfield, with required marker.")); + + + } +} + +/** * Test the tableselect form element for expected behavior. */ class FormsElementsTableSelectFunctionalTest extends DrupalWebTestCase { Index: modules/simpletest/tests/form_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v retrieving revision 1.8 diff -u -p -d -p -r1.8 form_test.module --- modules/simpletest/tests/form_test.module 17 Aug 2009 07:12:16 -0000 1.8 +++ modules/simpletest/tests/form_test.module 3 Sep 2009 00:44:32 -0000 @@ -65,6 +65,15 @@ function form_test_menu() { 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); + + $items['form_test/form-labels'] = array( + 'title' => 'Form label test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_label_test_form'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -365,3 +374,71 @@ function form_storage_test_form_submit($ $form_state['storage']['step']++; drupal_set_message("Form constructions: ". $_SESSION['constructions']); } + +/** + * A form for testing form labels. + */ +function form_label_test_form(&$form_state) { + $form['form_checkboxes_test'] = array( + '#type' => 'checkboxes', + '#title' => t('Checkboxes test'), + '#options' => array( + 'firstcheckbox' => t('First Checkbox'), + 'secondcheckbox' => t('Second Checkbox'), + 'thirdcheckbox' => t('Third Checkbox'), + ), + ); + $form['form_radios_test'] = array( + '#type' => 'radios', + '#title' => t('Radios test'), + '#options' => array( + 'firstradio' => t('First Radio'), + 'secondradio' => t('Second Radio'), + 'thirdradio' => t('Third Radio'), + ), + ); + $form['form_checkbox_test'] = array( + '#type' => 'checkbox', + '#title' => t('Checkbox test'), + ); + $form['form_checkbox_test_attribute'] = array( + '#type' => 'checkbox', + '#title' => t('Checkbox test with label as attribute'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE, + ); + $form['form_checkbox_test_no_label_option'] = array( + '#type' => 'checkbox', + '#title' => t('Checkbox test with label not styled as an option'), + '#label_option' => FALSE, + ); + $form['form_textfield_test_title_and_required'] = array( + '#type' => 'textfield', + '#title' => t('Textfield test for required with title'), + '#required' => TRUE, + ); + $form['form_textfield_test_required'] = array( + '#type' => 'textfield', + '#required' => TRUE, + ); + $form['form_textfield_test_title'] = array( + '#type' => 'textfield', + '#title' => t('Textfield test for title only'), + ); + $form['form_textfield_test_title_attribute'] = array( + '#type' => 'textfield', + '#title' => t('Textfield test for title as attribute'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE, + ); + $form['form_textfield_test_title_after'] = array( + '#type' => 'textfield', + '#title' => t('Textfield test for title as attribute'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_AFTER, + ); + $form['form_textfield_test_label_option'] = array( + '#type' => 'textfield', + '#title' => t('Textfield test for title with label styled as an option'), + '#label_option' => TRUE, + ); + + return $form; +} \ No newline at end of file Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.71 diff -u -p -d -p -r1.71 system.api.php --- modules/system/system.api.php 1 Sep 2009 17:40:27 -0000 1.71 +++ modules/system/system.api.php 3 Sep 2009 00:44:32 -0000 @@ -215,6 +215,9 @@ function hook_db_rewrite_sql($query, $pr * - "#pre_render": array of callback functions taking $element and $form_state. * - "#post_render": array of callback functions taking $element and $form_state. * - "#submit": array of callback functions taking $form and $form_state. + * - "#show_title": optionally one of the FORM_ELEMENT_SHOW_TITLE_* constants + * indicating how the #title should be added (defaults to before the element). + * - "#label_option": add a class to style the element label as an option. */ function hook_elements() { $type['filter_format'] = array('#input' => TRUE); Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.784 diff -u -p -d -p -r1.784 system.module --- modules/system/system.module 1 Sep 2009 16:50:12 -0000 1.784 +++ modules/system/system.module 3 Sep 2009 00:44:32 -0000 @@ -86,6 +86,20 @@ define('REGIONS_VISIBLE', 'visible'); */ define('REGIONS_ALL', 'all'); +/** + * Output form element titles as labels after form elements (rather than + * before the element, which is the default). + * @see system_elements(). + */ +define('FORM_ELEMENT_SHOW_TITLE_AFTER', 'after'); + +/** + * Output form element titles as the title attribute of form elements, rather + * than as a label. + * @see system_elements(). + */ +define('FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE', 'attribute'); + /** * Implement hook_help(). @@ -381,7 +395,8 @@ function system_elements() { '#process' => array('ajax_process_form'), '#theme' => 'radio', '#theme_wrappers' => array('form_element'), - '#form_element_skip_title' => TRUE, + '#show_title' => FORM_ELEMENT_SHOW_TITLE_AFTER, + '#label_option' => TRUE, ); $type['checkboxes'] = array( @@ -398,7 +413,8 @@ function system_elements() { '#process' => array('ajax_process_form'), '#theme' => 'checkbox', '#theme_wrappers' => array('form_element'), - '#form_element_skip_title' => TRUE, + '#show_title' => FORM_ELEMENT_SHOW_TITLE_AFTER, + '#label_option' => TRUE, ); $type['select'] = array(