Commit 99833c62 authored by Dries's avatar Dries

- Patch #558928 by brandonojc, mgifford, Owen Barton, Everett Zufelt: improved...

- Patch #558928 by brandonojc, mgifford, Owen Barton, Everett Zufelt: improved consistency, flexibility and accessibility of form element labels.
parent 92760988
......@@ -5579,6 +5579,9 @@ function drupal_common_theme() {
'form_required_marker' => array(
'arguments' => array('element' => NULL),
),
'form_element_label' => array(
'render element' => 'element',
),
'text_format_wrapper' => array(
'render element' => 'element',
),
......
......@@ -1065,6 +1065,7 @@ function form_builder($form_id, $element, &$form_state) {
$element += array(
'#required' => FALSE,
'#attributes' => array(),
'#title_display' => 'before',
);
// Special handling if we're on the top level form element.
......@@ -1687,7 +1688,6 @@ function form_options_flatten($array, $reset = TRUE) {
*/
function theme_select($variables) {
$element = $variables['element'];
$select = '';
$size = $element['#size'] ? ' size="' . $element['#size'] . '"' : '';
_form_set_class($element, array('form-select'));
$multiple = $element['#multiple'];
......@@ -1844,10 +1844,6 @@ function theme_radio($variables) {
$output .= (check_plain($element['#value']) == $element['#return_value']) ? ' checked="checked" ' : ' ';
$output .= drupal_attributes($element['#attributes']) . ' />';
if (isset($element['#title'])) {
$output = '<label class="option" for="' . $element['#id'] . '">' . $output . ' ' . $element['#title'] . '</label>';
}
return $output;
}
......@@ -2197,11 +2193,6 @@ function theme_checkbox($variables) {
}
$checkbox .= drupal_attributes($element['#attributes']) . ' />';
if (isset($element['#title'])) {
$required = !empty($element['#required']) ? ' <span class="form-required" title="' . $t('This field is required.') . '">*</span>' : '';
$checkbox = '<label class="option" for="' . $element['#id'] . '">' . $checkbox . ' ' . $element['#title'] . $required . '</label>';
}
return $checkbox;
}
......@@ -2235,6 +2226,15 @@ function theme_checkboxes($variables) {
* This is used as a pre render function for checkboxes and radios.
*/
function form_pre_render_conditional_form_element($element) {
// Set the element's title attribute to show #title as a tooltip, if needed.
if (isset($element['#title']) && $element['#title_display'] == 'attribute') {
$element['#attributes']['title'] = $element['#title'];
if (!empty($element['#required'])) {
// Append an indication that this field is required.
$element['#attributes']['title'] .= ' (' . $t('Required') . ')';
}
}
if (isset($element['#title']) || isset($element['#description'])) {
unset($element['#id']);
$element['#theme_wrappers'][] = 'form_element';
......@@ -2827,10 +2827,37 @@ function theme_file($variables) {
/**
* Theme a form element.
*
* Each form element is wrapped in a DIV with #type and #name classes. In
* addition to the element itself, the div contains a label before or after
* the element based on the optional #title_display property. After the label
* and fields this function outputs the optional element #description.
*
* The optional #title_display property can have these values:
* - before: The label is output before the element. This is the default.
* The label includes the #title and the required marker, if #required.
* - after: The label is output after the element. For example, this is used
* for radio and checkbox #type elements as set in system_element_info().
* If the #title is empty but the field is #required, the label will
* contain only the required marker.
* - attribute: Set the title attribute on the element to create a tooltip
* but output no label element. This is supported only for checkboxes
* and radios in form_pre_render_conditional_form_element(). It is used
* where a visual label is not needed, such as a table of checkboxes where
* the row and column provide the context. The tooltip will include the
* title and required marker.
*
* If the #title property is not set, then the label and any required marker
* will not be output, regardless of the #title_display or #required values.
* This can be useful in cases such as the password_confirm element, which
* creates children elements that have their own labels and required markers,
* but the parent element should have neither. Use this carefully because a
* field without an associated label can cause accessibility challenges.
*
* @param $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
* Properties used: #title, #description, #id, #required, #children
* Properties used: #title, #title_display, #description, #id, #required,
* #children, #type, #name.
*
* @return
* A string representing the form element.
......@@ -2851,20 +2878,30 @@ function theme_form_element($variables) {
$class[] = 'form-item-' . strtr($element['#name'], array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
}
// If #title is not set, we don't display any label or required marker.
if (!isset($element['#title'])) {
$element['#title_display'] = 'none';
}
$output = '<div class="' . implode(' ', $class) . '">' . "\n";
$required = !empty($element['#required']) ? theme('form_required_marker', array('element' => $element)) : '';
if (!empty($element['#title']) && empty($element['#form_element_skip_title'])) {
$title = $element['#title'];
if (!empty($element['#id'])) {
$output .= ' <label for="' . $element['#id'] . '">' . $t('!title !required', array('!title' => filter_xss_admin($title), '!required' => $required)) . "</label>\n";
}
else {
$output .= ' <label>' . $t('!title !required', array('!title' => filter_xss_admin($title), '!required' => $required)) . "</label>\n";
}
}
switch ($element['#title_display']) {
case 'before':
$output .= ' ' . theme('form_element_label', $variables);
$output .= ' ' . $element['#children'] . "\n";
break;
case 'after':
$output .= ' ' . $element['#children'];
$output .= ' ' . theme('form_element_label', $variables) . "\n";
break;
$output .= " " . $element['#children'] . "\n";
case 'none':
case 'attribute':
// Output no label and no required marker, only the children.
$output .= ' ' . $element['#children'] . "\n";
break;
}
if (!empty($element['#description'])) {
$output .= ' <div class="description">' . $element['#description'] . "</div>\n";
......@@ -2896,6 +2933,58 @@ function theme_form_required_marker($variables) {
return '<span' . drupal_attributes($attributes) . '>*</span>';
}
/**
* Theme a form element label and required marker.
*
* Form element labels include the #title and a #required marker. The label is
* associated with the element itself by the element #id. Labels may appear
* before or after elements, depending on theme_form_element() and #title_display.
*
* This function will not be called for elements with no labels, depending on
* #title_display. For elements that have an empty #title and are not required,
* this function will output no label (''). For required elements that have an
* empty #title, this will output the required marker alone within the label.
* The label will use the #id to associate the marker with the field that is
* required. That is especially important for screenreader users to know
* which field is required.
*
* @param $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
* Properties used: #required, #title, #id, #value, #description.
* @return
* A string representing the form element label.
*
* @ingroup themeable
*/
function theme_form_element_label($variables) {
$element = $variables['element'];
// This is also used in the installer, pre-database setup.
$t = get_t();
// If title and required marker are both empty, output no label.
if (empty($element['#title']) && empty($element['#required'])) {
return '';
}
// If the element is required, a required marker is appended to the label.
$required = !empty($element['#required']) ? theme('form_required_marker', array('element' => $element)) : '';
$title = filter_xss_admin($element['#title']);
$attributes = array();
if ($element['#title_display'] == 'after') {
// Style the label as class option to display inline with the element.
$attributes['class'] = 'option';
}
if (!empty($element['#id'])) {
$attributes['for'] = $element['#id'];
}
// The leading whitespace helps visually separate fields from inline labels.
return ' <label' . drupal_attributes($attributes) . '>' . $t('!title !required', array('!title' => $title, '!required' => $required)) . "</label>\n";
}
/**
* Sets a form element's class attribute.
*
......
......@@ -5,7 +5,7 @@
Drupal.behaviors.commentFieldsetSummaries = {
attach: function (context) {
$('fieldset#edit-comment-settings', context).setSummary(function (context) {
return Drupal.checkPlain($('input:checked', context).parent().text());
return Drupal.checkPlain($('input:checked', context).next('label').text());
});
// Provide the summary for the node type form.
$('fieldset#edit-comment', context).setSummary(function(context) {
......@@ -15,7 +15,7 @@ Drupal.behaviors.commentFieldsetSummaries = {
vals.push($("select[name='comment'] option:selected", context).text());
// Threading.
var threading = $("input[name='comment_default_mode']:checked", context).parent().text();
var threading = $("input[name='comment_default_mode']:checked", context).next('label').text();
if (threading) {
vals.push(threading);
}
......
......@@ -22,7 +22,7 @@ Drupal.behaviors.contentTypes = {
});
$('fieldset#edit-display', context).setSummary(function(context) {
var vals = [];
$('input:checked', context).parent().each(function() {
$('input:checked', context).next('label').each(function() {
vals.push(Drupal.checkPlain($(this).text()));
});
if (!$('#edit-node-submitted', context).is(':checked')) {
......
......@@ -132,25 +132,6 @@ function shortcut_set_switch_submit($form, &$form_state) {
shortcut_set_assign_user($set, $account);
}
/**
* Theme function for the form that switches shortcut sets.
*
* @param $variables
* An associative array containing:
* - form: An array representing the form.
* @return
* A themed HTML string representing the content of the form.
*
* @ingroup themeable
* @see shortcut_set_switch()
*/
function theme_shortcut_set_switch($variables) {
$form = $variables['form'];
// Render the textfield for adding a new set inline with the radio button.
$form['set']['new']['#title'] = t('New set: !textfield', array('!textfield' => drupal_render($form['new'])));
return drupal_render_children($form);
}
/**
* Menu callback; Build the form for customizing shortcut sets.
*
......
......@@ -82,3 +82,12 @@ div.add-or-remove-shortcuts a:hover span.text {
-webkit-border-bottom-right-radius: 5px;
}
#shortcut-set-switch .form-type-radios {
padding-bottom: 0;
margin-bottom: 0;
}
#shortcut-set-switch .form-item-new {
padding-top: 0;
padding-left: 17px;
}
......@@ -105,10 +105,6 @@ function shortcut_menu() {
*/
function shortcut_theme() {
return array(
'shortcut_set_switch' => array(
'render element' => 'form',
'file' => 'shortcut.admin.inc',
),
'shortcut_set_customize' => array(
'render element' => 'form',
'file' => 'shortcut.admin.inc',
......
......@@ -206,6 +206,62 @@ class FormValidationTestCase extends DrupalWebTestCase {
}
}
/**
* Test form element labels, required markers and associated output.
*/
class FormsElementsLabelsTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Form element and label output test',
'description' => 'Test form element labels, required markers and associated output.',
'group' => 'Form API',
);
}
function setUp() {
parent::setUp('form_test');
}
/**
* Test form elements, labels, title attibutes and required marks output
* correctly and have the correct label option class if needed.
*/
function testFormLabels() {
$this->drupalGet('form_test/form-labels');
// Check that the checkbox/radio processing is not interfering with
// basic placement.
$elements = $this->xpath('//input[@id="edit-form-checkboxes-test-third-checkbox"]/following-sibling::label[@for="edit-form-checkboxes-test-third-checkbox" 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-second-radio"]/following-sibling::label[@for="edit-form-radios-test-second-radio" and @class="option"]');
$this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for regular radios."));
// Exercise various defaults for checkboxes and modifications to ensure
// appropriate override and correct behaviour.
$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 by default."));
// Exercise various defaults for textboxes and modifications to ensure
// appropriate override and correct behaviour.
$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 inside label."));
$elements = $this->xpath('//input[@id="edit-form-textfield-test-no-title-required"]/preceding-sibling::label[@for="edit-form-textfield-test-no-title-required"]/span[@class="form-required"]');
$this->assertTrue(isset($elements[0]), t("Label tag with required marker preceeds required textfield with no title."));
$elements = $this->xpath('//input[@id="edit-form-textfield-test-title"]/preceding-sibling::span[@class="form-required"]');
$this->assertFalse(isset($elements[0]), t("No required marker on non-required field."));
$elements = $this->xpath('//input[@id="edit-form-textfield-test-title-after"]/following-sibling::label[@for="edit-form-textfield-test-title-after" and @class="option"]');
$this->assertTrue(isset($elements[0]), t("Label after field and label option class correct for text field."));
$elements = $this->xpath('//label[@for="edit-form-textfield-test-title-no-show"]');
$this->assertFalse(isset($elements[0]), t("No label tag when title set not to display."));
}
}
/**
* Test the tableselect form element for expected behavior.
*/
......
......@@ -94,6 +94,14 @@ function form_test_menu() {
'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;
}
......@@ -474,6 +482,62 @@ function form_test_storage_form_submit($form, &$form_state) {
drupal_set_message("Form constructions: ". $_SESSION['constructions']);
}
/**
* A form for testing form labels and required marks.
*/
function form_label_test_form(&$form_state) {
$form['form_checkboxes_test'] = array(
'#type' => 'checkboxes',
'#title' => t('Checkboxes test'),
'#options' => array(
'first-checkbox' => t('First checkbox'),
'second-checkbox' => t('Second checkbox'),
'third-checkbox' => t('Third checkbox'),
),
);
$form['form_radios_test'] = array(
'#type' => 'radios',
'#title' => t('Radios test'),
'#options' => array(
'first-radio' => t('First radio'),
'second-radio' => t('Second radio'),
'third-radio' => t('Third radio'),
),
);
$form['form_checkbox_test'] = array(
'#type' => 'checkbox',
'#title' => t('Checkbox test'),
);
$form['form_textfield_test_title_and_required'] = array(
'#type' => 'textfield',
'#title' => t('Textfield test for required with title'),
'#required' => TRUE,
);
$form['form_textfield_test_no_title_required'] = array(
'#type' => 'textfield',
// We use an empty title, since not setting #title supresses the label
// and required marker.
'#title' => '',
'#required' => TRUE,
);
$form['form_textfield_test_title'] = array(
'#type' => 'textfield',
'#title' => t('Textfield test for title only'),
// Not required.
);
$form['form_textfield_test_title_after'] = array(
'#type' => 'textfield',
'#title' => t('Textfield test for title after element'),
'#title_display' => 'after',
);
// Textfield test for title set not to display
$form['form_textfield_test_title_no_show'] = array(
'#type' => 'textfield',
);
return $form;
}
/**
* Menu callback; Invokes a form builder function with a wrapper callback.
*/
......
......@@ -320,6 +320,8 @@ function hook_cron_queue_info() {
* - "#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.
* - "#title_display": optional string indicating if and how #title should be
* displayed, see theme_form_element() and theme_form_element_label().
*
* @see hook_element_info_alter()
* @see system_element_info()
......
......@@ -56,7 +56,6 @@
*/
define('REGIONS_ALL', 'all');
/**
* Implement hook_help().
*/
......@@ -399,7 +398,7 @@ function system_element_info() {
'#process' => array('ajax_process_form'),
'#theme' => 'radio',
'#theme_wrappers' => array('form_element'),
'#form_element_skip_title' => TRUE,
'#title_display' => 'after',
);
$types['checkboxes'] = array(
'#input' => TRUE,
......@@ -414,7 +413,7 @@ function system_element_info() {
'#process' => array('ajax_process_form'),
'#theme' => 'checkbox',
'#theme_wrappers' => array('form_element'),
'#form_element_skip_title' => TRUE,
'#title_display' => 'after',
);
$types['select'] = array(
'#input' => TRUE,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment