diff --git a/core/includes/form.inc b/core/includes/form.inc
index 360155e..6e0d57b 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -191,10 +191,6 @@ function form_options_flatten($array) {
*
* Default template: select.html.twig.
*
- * It is possible to group options together; to do this, change the format of
- * $options to an associative array in which the keys are group labels, and the
- * values are associative arrays in the normal $options format.
- *
* @param $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
@@ -207,7 +203,41 @@ function template_preprocess_select(&$variables) {
Element\RenderElement::setAttributes($element, array('form-select'));
$variables['attributes'] = $element['#attributes'];
- $variables['options'] = form_select_options($element);
+ $variables['options'] = drupal_render_children($element);
+}
+
+/**
+ * Prepares variables for option group element templates.
+ *
+ * Default template: optgroup.html.twig.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: An associative array containing the properties of the element.
+ * Properties used: #title, #value, #options, #multiple, #attributes.
+ */
+function template_preprocess_optgroup(&$variables) {
+ $element = $variables['element'];
+
+ $variables['attributes'] = $element['#attributes'];
+ $variables['options'] = drupal_render_children($element);
+}
+
+/**
+ * Prepares variables for option element templates.
+ *
+ * Default template: option.html.twig.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: An associative array containing the properties of the element.
+ * Properties used: #title, #value, #attributes.
+ */
+function template_preprocess_option(&$variables) {
+ $element = $variables['element'];
+
+ $variables['attributes'] = $element['#attributes'];
+ $variables['label'] = $element['#title'];
}
/**
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index b349918..8b668b3 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -2396,6 +2396,14 @@ function drupal_common_theme() {
'render element' => 'element',
'template' => 'select',
),
+ 'option' => array(
+ 'render element' => 'element',
+ 'template' => 'option',
+ ),
+ 'optgroup' => array(
+ 'render element' => 'element',
+ 'template' => 'optgroup',
+ ),
'fieldset' => array(
'render element' => 'element',
'template' => 'fieldset',
diff --git a/core/lib/Drupal/Core/Render/Element/Optgroup.php b/core/lib/Drupal/Core/Render/Element/Optgroup.php
new file mode 100644
index 0000000..03bc474
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/Optgroup.php
@@ -0,0 +1,105 @@
+ FALSE,
+ '#process' => array(
+ array($class, 'processOptgroup'),
+ array($class, 'processAjaxForm'),
+ ),
+ '#theme' => 'optgroup',
+ '#options' => array(),
+ );
+ }
+
+ /**
+ * Processes an option group form element.
+ *
+ * This expands the #options property of option groups into proper option
+ * elements.
+ *
+ * @param array $element
+ * The form element to process. Properties used:
+ * - #options: (optional) An array of options for this option group where
+ * the keys are the option values and the values are the respective
+ * labels. Instead of specifying the options in this way, respective
+ * option elements can be created as child elements of an option group
+ * element.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array $complete_form
+ * The complete form structure.
+ *
+ * @return array
+ * The processed element.
+ *
+ * @see _form_validate()
+ */
+ public static function processOptgroup(&$element, FormStateInterface $form_state, &$complete_form) {
+ // @TODO This check is taken over from \Drupal\Core\Render\Element\Radios
+ // and \Drupal\Core\Render\Element\Checkboxes, but I do not understand why
+ // it is necessary.
+ if (count($element['#options']) > 0) {
+ // @TODO This is ported from form_select_options(). But instead of
+ // checking #value, #default_value should be used here and below, right?
+ $value_valid = isset($element['#value']) || array_key_exists('#value', $element);
+ $value_is_array = $value_valid && is_array($element['#value']);
+ // Check if the element is multiple select and no value has been selected.
+ $empty_value = (empty($element['#value']) && !empty($element['#multiple']));
+
+ $weight = 0;
+ foreach ($element['#options'] as $key => $choice) {
+ $key = (string) $key;
+
+ // Maintain order of options as defined in #options, in case the element
+ // defines custom option sub-elements, but does not define all option
+ // sub-elements.
+ $weight += 0.001;
+
+ $empty_choice = $empty_value && $key == '_none';
+ $is_default = $value_valid && ((!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value']))) || $empty_choice);
+
+ $element += array($key => array());
+ $element[$key] += array(
+ '#type' => 'option',
+ '#title' => $choice,
+ // The key is sanitized in Drupal\Core\Template\Attribute during output
+ // from the theme function.
+ '#return_value' => $key,
+ '#selected' => $value_valid && ((!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value']))) || $empty_choice),
+ '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
+ '#weight' => $weight,
+ );
+ }
+ }
+
+ return $element;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/Option.php b/core/lib/Drupal/Core/Render/Element/Option.php
new file mode 100644
index 0000000..9877d69
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/Option.php
@@ -0,0 +1,54 @@
+ FALSE,
+ '#process' => array(
+ array($class, 'processAjaxForm'),
+ ),
+ '#pre_render' => array(
+ array($class, 'preRenderOption'),
+ ),
+ '#theme' => 'option',
+ '#selected' => FALSE,
+ );
+ }
+
+ /**
+ * Prepares a select render element.
+ */
+ public static function preRenderOption($element) {
+ $element['#attributes']['value'] = $element['#return_value'];
+ if ($element['#selected']) {
+ $element['#attributes']['selected'] = 'selected';
+ }
+ return $element;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/Select.php b/core/lib/Drupal/Core/Render/Element/Select.php
index 70f14d6..08e3716 100644
--- a/core/lib/Drupal/Core/Render/Element/Select.php
+++ b/core/lib/Drupal/Core/Render/Element/Select.php
@@ -17,8 +17,11 @@
* a select element, including behavior if #required is TRUE or FALSE.
*
* @FormElement("select")
+ *
+ * @see \Drupal\Core\Render\Element\Option
+ * @see \Drupal\Core\Render\Element\Optgroup
*/
-class Select extends FormElement {
+class Select extends Optgroup {
/**
* {@inheritdoc}
@@ -113,6 +116,20 @@ public static function processSelect(&$element, FormStateInterface $form_state,
$element['#options'] = $empty_option + $element['#options'];
}
}
+
+ // If there are option group elements, pass the '#value' and '#multiple'
+ // keys down so they can properly set the selected option.
+ // @see \Drupal\Core\Render\Element\Optgroup::processOptgroup()
+ foreach (Element::children($element) as $child) {
+ if ($element[$child]['#type'] == 'optgroup') {
+ $element[$child]['#value'] = $element['#value'];
+ $element[$child]['#multiple'] = $element['#multiple'];
+ }
+ }
+
+ // Process the '#options' element in the same way that option groups do.
+ static::processOptgroup($element, $form_state, $complete_form);
+
return $element;
}
diff --git a/core/modules/system/templates/optgroup.html.twig b/core/modules/system/templates/optgroup.html.twig
new file mode 100644
index 0000000..1efe368
--- /dev/null
+++ b/core/modules/system/templates/optgroup.html.twig
@@ -0,0 +1,13 @@
+{#
+/**
+ * @file
+ * Default theme implementation for an optgroup element.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the optgroup tag.
+ * - options: The option element children.
+ *
+ * @ingroup themeable
+ */
+#}
+
diff --git a/core/modules/system/templates/option.html.twig b/core/modules/system/templates/option.html.twig
new file mode 100644
index 0000000..d795da9
--- /dev/null
+++ b/core/modules/system/templates/option.html.twig
@@ -0,0 +1,13 @@
+{#
+/**
+ * @file
+ * Default theme implementation for an option element.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the option tag.
+ * - label: The option label.
+ *
+ * @ingroup themeable
+ */
+#}
+