diff --git a/core/includes/form.inc b/core/includes/form.inc index 39ae1e2cf6..bacab5ef71 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -73,12 +73,12 @@ function form_select_options($element, $choices = NULL) { } $choices = $element['#options']; $sort_options = isset($element['#sort_options']) && $element['#sort_options']; - $sort_start = isset($element['#sort_start']) ? $element['#sort_start'] : 0; + $sort_start = $element['#sort_start'] ?? 0; } else { // We are within an option group. $sort_options = isset($choices['#sort_options']) && $choices['#sort_options']; - $sort_start = isset($choices['#sort_start']) ? $choices['#sort_start'] : 0; + $sort_start = $choices['#sort_start'] ?? 0; unset($choices['#sort_options']); unset($choices['#sort_start']); } diff --git a/core/lib/Drupal/Core/Render/Element/Select.php b/core/lib/Drupal/Core/Render/Element/Select.php index 342b763f6e..7394614bb9 100644 --- a/core/lib/Drupal/Core/Render/Element/Select.php +++ b/core/lib/Drupal/Core/Render/Element/Select.php @@ -31,10 +31,10 @@ * options by their labels, after rendering and translation is complete. * Can be set within an option group to sort that group. * - #sort_start: (optional) Option index to start sorting at, where 0 is the - * first option (default is 0, and this only applies if #sort_options is - * TRUE). Can be used within an option group. If an empty option is being - * added automatically (see #empty_option and #empty_value properties), and - * you want it to stay at the top of the list, be sure to start sorting at 1. + * first option. Can be used within an option group. If an empty option is + * being added automatically (see #empty_option and #empty_value properties), + * this defaults to 1 to keep the empty option at the top of the list. + * Otherwise, it defaults to 0. * - #empty_option: (optional) The label to show for the first default option. * By default, the label is automatically set to "- Select -" for a required * field and "- None -" for an optional field. @@ -90,7 +90,7 @@ public function getInfo() { '#input' => TRUE, '#multiple' => FALSE, '#sort_options' => FALSE, - '#sort_start' => 0, + '#sort_start' => NULL, '#process' => [ [$class, 'processSelect'], [$class, 'processAjaxForm'], @@ -152,6 +152,9 @@ public static function processSelect(&$element, FormStateInterface $form_state, $element['#options'] = $empty_option + $element['#options']; } } + // Provide the correct default value for #sort_start. + $element['#sort_start'] = $element['#sort_start'] ?? + ((isset($element['#empty_value'])) ? 1 : 0); return $element; } diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php index e4ab781468..cd8542c68e 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php @@ -133,7 +133,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { ]; // Add a select to test sorting at the top level, and with some of the - // option groups sorted and some left alone. + // option groups sorted, some left alone, and at least one with sort start + // set to a non-default value. $sortable_options = $this->makeSortableOptions('sso'); $sortable_options['sso_zzgroup']['#sort_options'] = TRUE; $sortable_options['sso_xxgroup']['#sort_options'] = TRUE; @@ -157,6 +158,17 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#empty_value' => 'sno_empty', ]; + // Add a select to test sorting with a -NONE- option included, + // and sort start not set. + $sortable_none_nostart_options = $this->makeSortableOptions('snn'); + $sortable_none_nostart_options['snn_zzgroup']['#sort_options'] = TRUE; + $form['sorted_none_nostart'] = [ + '#type' => 'select', + '#options' => $sortable_none_nostart_options, + '#sort_options' => TRUE, + '#empty_value' => 'snn_empty', + ]; + $form['submit'] = ['#type' => 'submit', '#value' => 'Submit']; return $form; } @@ -172,9 +184,9 @@ public function buildForm(array $form, FormStateInterface $form_state) { */ protected function makeSortableOptions($prefix) { return [ - // Don't use $this->t() here, because we really don't want these - // to be translated or added to localize.d.o. Using TranslatableMarkup - // in places tests that casting to string is working, however. + // Don't use $this->t() here, to avoid adding strings to + // localize.drupal.org. Do use TranslatableMarkup in places, to test + // that labels are cast to strings before sorting. $prefix . '_first_element' => new TranslatableMarkup('first element'), $prefix . '_second' => new TranslatableMarkup('second element'), $prefix . '_zzgroup' => [ diff --git a/core/modules/system/tests/src/Functional/Form/FormTest.php b/core/modules/system/tests/src/Functional/Form/FormTest.php index 7df29f31ae..0551c88322 100644 --- a/core/modules/system/tests/src/Functional/Form/FormTest.php +++ b/core/modules/system/tests/src/Functional/Form/FormTest.php @@ -535,6 +535,28 @@ public function testSelectSorting() { 'sno_gf', ]); + $this->validateSelectSorting('sorted_none_nostart', [ + 'snn_empty', + 'snn_a', + 'snn_d', + 'snn_first_element', + 'snn_b', + 'snn_c', + 'snn_second', + 'snn_xxgroup', + 'snn_gz', + 'snn_gi', + 'snn_gh', + 'snn_yygroup', + 'snn_ge', + 'snn_gd', + 'snn_gf', + 'snn_zzgroup', + 'snn_ga', + 'snn_gb', + 'snn_gc', + ]); + // Verify that #sort_order and #sort_start are not in the page. $this->assertSession()->responseNotContains('#sort_order'); $this->assertSession()->responseNotContains('#sort_start');