diff --git a/core/includes/form.inc b/core/includes/form.inc
index d602d54..967b88c 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -1854,6 +1854,7 @@ function form_builder($form_id, &$element, &$form_state) {
'#required' => FALSE,
'#attributes' => array(),
'#title_display' => 'before',
+ '#description_display' => 'after',
);
// Special handling if we're on the top level form element.
@@ -4636,19 +4637,23 @@ function form_process_file($element) {
* $form structure and set via form_builder()).
* - form-disabled: Only set if the form element is #disabled.
*
- * In addition to the element itself, the DIV contains a label for the element
- * based on the optional #title_display property, and an optional #description.
+ * In addition to the element itself, the DIV contains the element's label
+ * consisting of the #title property and an optional #required marker. It also
+ * contains an optional #description. While by default, the label is displayed
+ * before the element and the description right after the element, this can be
+ * modified by the optional #title_display and #description_display properties.
*
* 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
+ * - before (default): The label is displayed before the element. The label
+ * includes the #title and the required marker, if #required.
+ * - after: The label is displayed 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.
- * - invisible: Labels are critical for screen readers to enable them to
- * properly navigate through forms but can be visually distracting. This
- * property hides the label for everyone except screen readers.
+ * - invisible: While the label isn't displayed by visual browsers, it is
+ * available to screen readers which need it for properly navigating through
+ * forms. It is also available to validation functions that might set an error
+ * on the field that failed validation.
* - 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
@@ -4661,13 +4666,25 @@ function form_process_file($element) {
* 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.
+ * field without an associated label can cause accessibility challenges. In most
+ * cases where displaying a title would be visually distracting, it is strongly
+ * recommended to supply a title but render it invisible or as attribute.
+ *
+ * The optional #description_display property can have these values:
+ * - before: The description is output before the element. This may be used for
+ * elements that occupy very much vertical screen space.
+ * - after (default): The description is output after the element.
+ * - invisible: While the description isn't displayed by visual browsers, it is
+ * available to screen readers.
+ *
+ * If the #description property is not set, the description will not be output,
+ * regardless of the #description_display value.
*
* @param $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
- * Properties used: #title, #title_display, #description, #id, #required,
- * #children, #type, #name.
+ * Properties used: #title, #title_display, #description,
+ * #description_display, #id, #required, #children, #type, #name.
*
* @ingroup themeable
*/
@@ -4678,6 +4695,7 @@ function theme_form_element($variables) {
// may not necessarily have been processed by form_builder().
$element += array(
'#title_display' => 'before',
+ '#description_display' => 'after',
);
// Take over any #wrapper_attributes defined by the element.
@@ -4704,41 +4722,36 @@ function theme_form_element($variables) {
}
$output = '
' . "\n";
- // If #title is not set, we don't display any label or required marker.
+ // If #title is not set, we don't even call theme_form_element_label.
if (!isset($element['#title'])) {
$element['#title_display'] = 'none';
}
- $prefix = isset($element['#field_prefix']) ? '
' . $element['#field_prefix'] . ' ' : '';
- $suffix = isset($element['#field_suffix']) ? '
' . $element['#field_suffix'] . '' : '';
+ // If #description is not set, we don't even call
+ // theme_form_element_description.
+ if (!isset($element['#description'])) {
+ $element['#description_display'] == 'none';
+ }
- switch ($element['#title_display']) {
- case 'before':
- case 'invisible':
- $output .= ' ' . theme('form_element_label', $variables);
- $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n";
- break;
+ if ($element['#title_display'] == 'before' || $element['#title_display'] == 'invisible') {
+ $output .= ' ' . theme('form_element_label', $variables);
+ }
+ if ($element['#description_display'] == 'before') {
+ $output .= "\n" . theme('form_element_description', $variables) . "\n";
+ }
- case 'after':
- $output .= ' ' . $prefix . $element['#children'] . $suffix;
- $output .= ' ' . theme('form_element_label', $variables) . "\n";
- break;
+ // Render the element's children.
+ $output .= isset($element['#field_prefix']) ? '
' . $element['#field_prefix'] . ' ' : ' ';
+ $output .= $element['#children'];
+ $output .= isset($element['#field_suffix']) ? '
' . $element['#field_suffix'] . '' : ' ';
- case 'none':
- case 'attribute':
- // Output no label and no required marker, only the children.
- $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n";
- break;
+ if ($element['#title_display'] == 'after') {
+ $output .= ' ' . theme('form_element_label', $variables);
}
-
- if (!empty($element['#description'])) {
- $attributes = array('class' => 'description');
- if (!empty($element['#id'])) {
- $attributes['id'] = $element['#id'] . '--description';
- }
- $output .= '
' . $element['#description'] . "
\n";
+ if ($element['#description_display'] == 'after' || $element['#description_display'] == 'invisible') {
+ $output .= "\n" . theme('form_element_description', $variables) . "\n";
}
- $output .= "
\n";
+ $output .= '' . "\n";
return $output;
}
@@ -4779,7 +4792,7 @@ function theme_form_required_marker($variables) {
* @param $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
- * Properties used: #required, #title, #id, #value, #description.
+ * Properties used: #id, #title, #title_display, #required.
*
* @ingroup themeable
*/
@@ -4814,6 +4827,45 @@ function theme_form_element_label($variables) {
}
/**
+ * Prepares variables for form element description templates.
+ *
+ * Default template: form-element-description.html.twig
+ *
+ * The description is associated with the element itself by the element #id.
+ * While by default displayed after the form element, the #description_display
+ * property may specify it to be displayed before the form element or to be
+ * visually hidden.
+ * For elements with no #description, this function will not be called.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: An associative array containing the properties of the element.
+ * Properties used: #id, #description, #description_display.
+ */
+function template_preprocess_form_element_description(&$variables) {
+ $element = $variables['element'];
+
+ // If description is empty, don't output anything.
+ if (!isset($element['#description']) || $element['#description'] === '') {
+ $variables['description'] = $variables['attributes'] = NULL;
+ }
+ else {
+ $attributes = array();
+ if ($element['#description_display'] == 'invisible') {
+ $attributes['class'] = 'visually-hidden';
+ }
+ else {
+ $attributes['class'] = 'description';
+ }
+ if (!empty($element['#id'])) {
+ $attributes['id'] = $element['#id'] . '--description';
+ }
+ $variables['attributes'] = $attributes;
+ $variables['description'] = $element['#description'];
+ }
+}
+
+/**
* Sets a form element's class attribute.
*
* Adds 'required' and 'error' classes as needed.
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 55ec85f..4af27bc 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -3215,6 +3215,10 @@ function drupal_common_theme() {
'form_element_label' => array(
'render element' => 'element',
),
+ 'form_element_description' => array(
+ 'render element' => 'element',
+ 'template' => 'form-element-description',
+ ),
'vertical_tabs' => array(
'render element' => 'element',
),
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index a8dcd13..2a8aaf3 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -236,6 +236,8 @@ function hook_queue_info_alter(&$queues) {
* - "#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().
+ * - "#description_display": optional string indicating how the #description
+ * should be displayed, see theme_form_element().
*
* @see hook_element_info_alter()
* @see system_element_info()
diff --git a/core/modules/system/templates/form-element-description.html.twig b/core/modules/system/templates/form-element-description.html.twig
new file mode 100644
index 0000000..d35f253
--- /dev/null
+++ b/core/modules/system/templates/form-element-description.html.twig
@@ -0,0 +1,22 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a form element description.
+ *
+ * This will not be called if the description is empty.
+ *
+ * Available variables:
+ * - description: The text of the description.
+ * - attributes: A list of HTML attributes for the description.
+ *
+ * @see template_preprocess()
+ * @see template_preprocess_form_description()
+ *
+ * @ingroup themeable
+ */
+#}
+{% if (description is not empty) %}
+
+ {{ description }}
+
+{% endif %}