diff --git a/src/Hook/ViewsSecondaryRowHooks.php b/src/Hook/ViewsSecondaryRowHooks.php
new file mode 100644
index 0000000..e80050d
--- /dev/null
+++ b/src/Hook/ViewsSecondaryRowHooks.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\views_secondary_row\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_secondary_row.
+ */
+class ViewsSecondaryRowHooks {
+  /**
+   * @file
+   * Adds a new Views Row style.
+   */
+
+  /**
+   * Implements hook_theme().
+   *
+   * Register views theming functions and those that are defined via views plugin
+   * definitions.
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) {
+    $theme_info = [
+      'views_secondary_row_style_plugin_table' => [
+        'render element' => 'form',
+        'file' => 'views_secondary_row.theme.inc',
+      ],
+      'views_secondary_row_view_table' => [
+        'render element' => 'form',
+        'file' => 'views_secondary_row.theme.inc',
+      ],
+    ];
+    return $theme_info;
+  }
+
+}
diff --git a/src/Hook/ViewsSecondaryRowThemeHooks.php b/src/Hook/ViewsSecondaryRowThemeHooks.php
new file mode 100644
index 0000000..0161ae3
--- /dev/null
+++ b/src/Hook/ViewsSecondaryRowThemeHooks.php
@@ -0,0 +1,502 @@
+<?php
+
+namespace Drupal\views_secondary_row\Hook;
+
+use Drupal\Core\Render\Element;
+use Drupal\Core\Template\Attribute;
+use Drupal\Core\Url;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Utility\TableSort;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for views_secondary_row.
+ */
+class ViewsSecondaryRowThemeHooks {
+  use StringTranslationTrait;
+  /**
+   * Prepares variables for views table templates.
+   *
+   * Copied from views\theme\theme.inc.
+   * Version history: Aligned with Views 9.4.x.
+   * Version history: Aligned with Views 7.x-3.6, not changed in 3.13.
+   * Default template: views-view-table.html.twig.
+   *
+   * @param array $variables
+   *   An associative array containing:
+   *   - view: A ViewExecutable object.
+   *   - rows: The raw row data.
+   */
+
+  /**
+   * Implements hook_preprocess_HOOK().
+   *
+   * Display a view as a table style.
+   *
+   * Copied from views\theme\theme.inc.
+   * Version history: Aligned with Views 9.4.x.
+   * Version history: Aligned with Views 7.x-3.6, not changed in 3.13.
+   */
+  #[Hook('preprocess_views_secondary_row_view_table', module: 'template')]
+  public function templatePreprocessViewsSecondaryRowViewTable(&$variables) {
+    /** @var \Drupal\views\Plugin\views\style\Table $handler */
+    $view = $variables['view'];
+    // We need the raw data for this grouping, which is passed in
+    // as $variables['rows'].
+    // However, the template also needs to use for the rendered fields. We
+    // therefore swap the raw data out to a new variable
+    // and reset $variables['rows'] so that it can get rebuilt.
+    // Store rows so that they may be used by further preprocess functions.
+    $result = $variables['result'] = $variables['rows'];
+    $variables['rows'] = [];
+    $variables['header'] = [];
+    $options = $view->style_plugin->options;
+    $handler = $view->style_plugin;
+    $fields =& $view->field;
+    $columns = $handler->sanitizeColumns($options['columns'], $fields);
+    $active = !empty($handler->active) ? $handler->active : '';
+    $order = !empty($handler->order) ? $handler->order : 'asc';
+    // A boolean variable which stores whether the table has a responsive class.
+    $responsive = FALSE;
+    // Check if secondary rows are specified.
+    // If not, just call the default 'table' preprocessing.
+    $has_secondary_row = FALSE;
+    foreach ($columns as $field => $column) {
+      if (!empty($options['info'][$field]['break2'])) {
+        $has_secondary_row = TRUE;
+      }
+    }
+    $query = TableSort::getQueryParameters(\Drupal::request());
+    if (isset($view->exposed_raw_input)) {
+      $query += $view->exposed_raw_input;
+    }
+    // A boolean to store whether the table's header has any labels.
+    $has_header_labels = FALSE;
+    foreach ($columns as $field => $column) {
+      // Create a second variable so we can easily find what fields we have and
+      // what the CSS classes should be.
+      $variables['fields'][$field] = Html::cleanCssIdentifier($field);
+      if ($active == $field) {
+        $variables['fields'][$field] .= ' is-active';
+      }
+      // Render the header labels.
+      // Skip the header labels that are broken into the secondary row.
+      if ($field == $column && empty($fields[$field]->options['exclude']) && empty($options['info'][$field]['break2'])) {
+        $label = !empty($fields[$field]) ? $fields[$field]->label() : '';
+        if (empty($options['info'][$field]['sortable']) || !$fields[$field]->clickSortable()) {
+          $variables['header'][$field]['content'] = $label;
+        }
+        else {
+          $initial = !empty($options['info'][$field]['default_sort_order']) ? $options['info'][$field]['default_sort_order'] : 'asc';
+          if ($active == $field) {
+            $initial = $order == 'asc' ? 'desc' : 'asc';
+          }
+          $title = $this->t('sort by @s', [
+            '@s' => $label,
+          ]);
+          if ($active == $field) {
+            $variables['header'][$field]['sort_indicator'] = [
+              '#theme' => 'tablesort_indicator',
+              '#style' => $initial,
+            ];
+          }
+          $query['order'] = $field;
+          $query['sort'] = $initial;
+          $link_options = [
+            'query' => $query,
+          ];
+          // It is ok to specify no URL path here as we will always reload the
+          // current page.
+          $url = new Url('<none>', [], $link_options);
+          $variables['header'][$field]['url'] = $url->toString();
+          $variables['header'][$field]['content'] = $label;
+          $variables['header'][$field]['title'] = $title;
+        }
+        $variables['header'][$field]['default_classes'] = $fields[$field]->options['element_default_classes'];
+        // Set up the header label class.
+        $variables['header'][$field]['attributes'] = [];
+        $class = $fields[$field]->elementLabelClasses(0);
+        if ($class) {
+          $variables['header'][$field]['attributes']['class'][] = $class;
+        }
+        // Add responsive header classes.
+        if (!empty($options['info'][$field]['responsive'])) {
+          $variables['header'][$field]['attributes']['class'][] = $options['info'][$field]['responsive'];
+          $responsive = TRUE;
+        }
+        // Add a CSS align class to each field if one was set.
+        if (!empty($options['info'][$field]['align'])) {
+          $variables['header'][$field]['attributes']['class'][] = Html::cleanCssIdentifier($options['info'][$field]['align']);
+        }
+        // Add a header label wrapper if one was selected.
+        if ($variables['header'][$field]['content']) {
+          $element_label_type = $fields[$field]->elementLabelType(TRUE, TRUE);
+          if ($element_label_type) {
+            $variables['header'][$field]['wrapper_element'] = $element_label_type;
+          }
+          // Improves accessibility of complex tables.
+          $variables['header'][$field]['attributes']['id'] = Html::getUniqueId('view-' . $field . '-table-column');
+        }
+        // Check if header label is not empty.
+        if (!empty($variables['header'][$field]['content'])) {
+          $has_header_labels = TRUE;
+        }
+        $variables['header'][$field]['attributes'] = new Attribute($variables['header'][$field]['attributes']);
+      }
+      // Add a CSS align class to each field if one was set.
+      if (!empty($options['info'][$field]['align'])) {
+        $variables['fields'][$field] .= ' ' . Html::cleanCssIdentifier($options['info'][$field]['align']);
+      }
+      // Render each field into its appropriate column.
+      foreach ($result as $num => $row) {
+        // Skip building the attributes and content if the field is to be excluded
+        // from the display.
+        if (!empty($fields[$field]->options['exclude'])) {
+          continue;
+        }
+        // Make all rows even rows,
+        // so that there's an empty odd/secondary row between them,
+        // where the "secondary row" fields can be placed.
+        $old_num = $num;
+        $primary_row_num = $num * 2;
+        $secondary_row_num = $primary_row_num + 1;
+        $num = $primary_row_num;
+        if (!empty($options['info'][$field]['break2'])) {
+          $num = $secondary_row_num;
+          $column = $options['info'][$field]['break2'];
+        }
+        // Reference to the column in the loop to make the code easier to read.
+        $column_reference =& $variables['rows'][$num]['columns'][$column];
+        $column_reference['default_classes'] = $fields[$field]->options['element_default_classes'];
+        // Set the field key to the column so it can be used for adding classes
+        // in a template.
+        $column_reference['fields'][] = $variables['fields'][$field];
+        // Add field classes.
+        if (!isset($column_reference['attributes'])) {
+          $column_reference['attributes'] = [];
+        }
+        if ($classes = $fields[$field]->elementClasses($num)) {
+          $column_reference['attributes']['class'][] = $classes;
+        }
+        // Add rowspan to primary row if it is configured as 2.
+        if ($num === $primary_row_num && isset($options['info'][$field]['rowspan1']) && (int) $options['info'][$field]['rowspan1'] === 2) {
+          $column_reference['attributes']['rowspan'][] = 2;
+        }
+        // Add colspan to secondary row.
+        if ($num == $secondary_row_num && !empty($options['info'][$field]['break2']) && !empty($options['info'][$field]['colspan2'])) {
+          $column_reference['attributes']['colspan'][] = $options['info'][$field]['colspan2'];
+        }
+        // Add responsive header classes.
+        if (!empty($options['info'][$field]['responsive'])) {
+          $column_reference['attributes']['class'][] = $options['info'][$field]['responsive'];
+        }
+        // Improves accessibility of complex tables.
+        if (isset($variables['header'][$field]['attributes']['id'])) {
+          $column_reference['attributes']['headers'] = [
+            $variables['header'][$field]['attributes']['id'],
+          ];
+        }
+        if (!empty($fields[$field])) {
+          // Modification for secondary row. Use $old_num, else empty rows appear.
+          $field_output = $handler->getField($old_num, $field);
+          $column_reference['wrapper_element'] = $fields[$field]->elementType(TRUE, TRUE);
+          if (!isset($column_reference['content'])) {
+            $column_reference['content'] = [];
+          }
+          // Only bother with separators and stuff if the field shows up.
+          // Place the field into the column, along with an optional separator.
+          if (trim($field_output) != '') {
+            if ($num == $secondary_row_num && !empty($options['info'][$field]['break2'])) {
+              if (!empty($options['info'][$column]['separator2'])) {
+                $column_reference['content'][] = [
+                  'separator' => [
+                    '#markup' => $options['info'][$column]['separator2'],
+                  ],
+                  'field_output' => [
+                    '#markup' => $field_output,
+                  ],
+                ];
+              }
+              else {
+                $column_reference['content'][] = [
+                  'field_output' => [
+                    '#markup' => $field_output,
+                  ],
+                ];
+              }
+            }
+            elseif (!empty($column_reference['content']) && !empty($options['info'][$column]['separator'])) {
+              $column_reference['content'][] = [
+                'separator' => [
+                  '#markup' => $options['info'][$column]['separator'],
+                ],
+                'field_output' => [
+                  '#markup' => $field_output,
+                ],
+              ];
+            }
+            else {
+              $column_reference['content'][] = [
+                'field_output' => [
+                  '#markup' => $field_output,
+                ],
+              ];
+            }
+          }
+        }
+        $column_reference['attributes'] = new Attribute($column_reference['attributes']);
+      }
+      // Remove columns if the "empty_column" option is checked and the
+      // field is empty.
+      if (!empty($options['info'][$field]['empty_column'])) {
+        $empty = TRUE;
+        foreach ($variables['rows'] as $columns) {
+          $empty &= empty($columns['columns'][$column]['content']);
+        }
+        if ($empty) {
+          foreach ($variables['rows'] as &$column_items) {
+            unset($column_items['columns'][$column]);
+          }
+          unset($variables['header'][$column]);
+        }
+      }
+    }
+    // Reorder rows, since odd/secondary rows are appended at the end of the list.
+    ksort($variables['rows']);
+    // Reorder columns.
+    // Sort order of fields in secondary rows may differ from target columns.
+    foreach ($variables['rows'] as $num => $row) {
+      if ($num % 2 == 0) {
+        continue;
+      }
+      $ordered = [];
+      foreach ($columns as $field => $column) {
+        if (isset($row['columns'][$column])) {
+          $ordered['columns'][$column] = $row['columns'][$column];
+        }
+      }
+      $variables['rows'][$num]['columns'] = $ordered['columns'];
+    }
+    // Hide table header if all labels are empty.
+    if (!$has_header_labels) {
+      $variables['header'] = [];
+    }
+    foreach ($variables['rows'] as $num => $row) {
+      $variables['rows'][$num]['attributes'] = [];
+      if ($row_class = $handler->getRowClass($num)) {
+        $variables['rows'][$num]['attributes']['class'][] = $row_class;
+      }
+      $emptyRow = TRUE;
+      foreach ($row['columns'] as $field => $column) {
+        if (!empty($column['content'])) {
+          $emptyRow = FALSE;
+          continue;
+        }
+      }
+      if ($emptyRow) {
+        $variables['rows'][$num]['attributes']['class'][] = 'views-secondary-row--no-content hidden';
+      }
+      $variables['rows'][$num]['attributes'] = new Attribute($variables['rows'][$num]['attributes']);
+    }
+    if (empty($variables['rows']) && !empty($options['empty_table'])) {
+      $build = $view->display_handler->renderArea('empty');
+      $variables['rows'][0]['columns'][0]['content'][0]['field_output'] = $build;
+      $variables['rows'][0]['attributes'] = new Attribute([
+        'class' => [
+          'odd',
+        ],
+      ]);
+      // Calculate the amounts of rows with output.
+      $variables['rows'][0]['columns'][0]['attributes'] = new Attribute([
+        'colspan' => count($variables['header']),
+        'class' => [
+          'views-empty',
+        ],
+      ]);
+    }
+    $variables['sticky'] = FALSE;
+    if (!empty($options['sticky'])) {
+      $variables['view']->element['#attached']['library'][] = 'core/drupal.tableheader';
+      $variables['sticky'] = TRUE;
+    }
+    // Add the caption to the list if set.
+    if (!empty($handler->options['caption'])) {
+      $variables['caption'] = [
+        '#markup' => $handler->options['caption'],
+      ];
+      $variables['caption_needed'] = TRUE;
+    }
+    elseif (!empty($variables['title'])) {
+      $variables['caption'] = [
+        '#markup' => $variables['title'],
+      ];
+      $variables['caption_needed'] = TRUE;
+    }
+    else {
+      $variables['caption'] = '';
+      $variables['caption_needed'] = FALSE;
+    }
+    // For backwards compatibility, initialize the 'summary' and 'description'
+    // variables, although core templates now all use 'summary_element' instead.
+    $variables['summary'] = $handler->options['summary'];
+    $variables['description'] = $handler->options['description'];
+    if (!empty($handler->options['summary']) || !empty($handler->options['description'])) {
+      $variables['summary_element'] = [
+        '#type' => 'details',
+        '#title' => $handler->options['summary'],
+            // To ensure that the description is properly escaped during rendering,
+            // use an 'inline_template' to let Twig do its magic, instead of 'markup'.
+        'description' => [
+          '#type' => 'inline_template',
+          '#template' => '{{ description }}',
+          '#context' => [
+            'description' => $handler->options['description'],
+          ],
+        ],
+      ];
+      $variables['caption_needed'] = TRUE;
+    }
+    $variables['responsive'] = FALSE;
+    // If the table has headers and it should react responsively to columns hidden
+    // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM
+    // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors.
+    if (isset($variables['header']) && $responsive) {
+      $variables['view']->element['#attached']['library'][] = 'core/drupal.tableresponsive';
+      // Add 'responsive-enabled' class to the table to identify it for JS.
+      // This is needed to target tables constructed by this function.
+      $variables['responsive'] = TRUE;
+    }
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK().
+   *
+   * Prepares variables for style plugin table templates.
+   *
+   * Copied from views_ui.theme.inc, and 3 columns inserted.
+   * This shows the Views UI OptionsForm.
+   * Version history: Aligned with Views 9.4.x.
+   * Version history: Aligned with Views 8.0.1.
+   *
+   * Default template: views-ui-style-plugin-table.html.twig.
+   */
+  #[Hook('preprocess_views_secondary_row_style_plugin_table', module: 'template')]
+  public function templatePreprocessViewsSecondaryRowStylePluginTable(&$variables) {
+    // @todo First, call the default views_ui function, then change
+    // template_preprocess_views_ui_style_plugin_table($variables).
+    $form = $variables['form'];
+    $header = [
+      $this->t('Field'),
+      $this->t('Column'),
+      $this->t('Align'),
+      $this->t('Separator'),
+          /* Begin insert views_secondary_row. */
+          [
+            'data' => $this->t('1st row Rowspan'),
+            'align' => 'center',
+          ],
+          [
+            'data' => $this->t('Secondary row'),
+            'align' => 'center',
+          ],
+          [
+            'data' => $this->t('2nd row Separator'),
+            'align' => 'center',
+          ],
+          [
+            'data' => $this->t('2nd row Colspan'),
+            'align' => 'center',
+          ],
+          /* End insert views_secondary_row. */
+          [
+            'data' => $this->t('Sortable'),
+            'align' => 'center',
+          ],
+          [
+            'data' => $this->t('Default order'),
+            'align' => 'center',
+          ],
+          [
+            'data' => $this->t('Default sort'),
+            'align' => 'center',
+          ],
+          [
+            'data' => $this->t('Hide empty column'),
+            'align' => 'center',
+          ],
+          [
+            'data' => $this->t('Responsive'),
+            'align' => 'center',
+          ],
+    ];
+    $rows = [];
+    foreach (Element::children($form['columns']) as $id) {
+      $row = [];
+      $row[]['data'] = $form['info'][$id]['name'];
+      $row[]['data'] = $form['columns'][$id];
+      $row[]['data'] = $form['info'][$id]['align'];
+      $row[]['data'] = $form['info'][$id]['separator'];
+      /* Begin insert views_secondary_row. */
+      $row[]['data'] = $form['info'][$id]['rowspan1'];
+      $row[]['data'] = $form['info'][$id]['break2'];
+      $row[]['data'] = $form['info'][$id]['separator2'];
+      $row[]['data'] = $form['info'][$id]['colspan2'];
+      /* End insert views_secondary_row. */
+      if (!empty($form['info'][$id]['sortable'])) {
+        $row[] = [
+          'data' => $form['info'][$id]['sortable'],
+          'align' => 'center',
+        ];
+        $row[] = [
+          'data' => $form['info'][$id]['default_sort_order'],
+          'align' => 'center',
+        ];
+        $row[] = [
+          'data' => $form['default'][$id],
+          'align' => 'center',
+        ];
+      }
+      else {
+        $row[] = '';
+      }
+      $row[] = [
+        'data' => $form['info'][$id]['empty_column'],
+        'align' => 'center',
+      ];
+      $row[] = [
+        'data' => $form['info'][$id]['responsive'],
+        'align' => 'center',
+      ];
+      $rows[] = $row;
+    }
+    // Add the special 'None' row.
+    $rows[] = [
+          [
+            'data' => $this->t('None'),
+              /* Begin replace views_secondary_row colspan from 6 to new value. */
+            'colspan' => 10,
+          ],
+          [
+            'align' => 'center',
+            'data' => $form['default'][-1],
+          ],
+          [
+            'colspan' => 2,
+          ],
+    ];
+    // Unset elements from the form array that are used to build the table so that
+    // they are not rendered twice.
+    unset($form['default']);
+    unset($form['info']);
+    unset($form['columns']);
+    $variables['table'] = [
+      '#type' => 'table',
+      '#theme' => 'table__views_ui_style_plugin_table',
+      '#header' => $header,
+      '#rows' => $rows,
+    ];
+    $variables['form'] = $form;
+  }
+
+}
diff --git a/views_secondary_row.info.yml b/views_secondary_row.info.yml
index 456e53f..2dee063 100644
--- a/views_secondary_row.info.yml
+++ b/views_secondary_row.info.yml
@@ -1,6 +1,6 @@
 name: 'Views Secondary Row'
 description: 'Provides a table style allowing fields to be broken into a secondary row.'
-core_version_requirement: ^8 || ^9 || ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 
 dependencies:
   - drupal:views
diff --git a/views_secondary_row.module b/views_secondary_row.module
index 51c6861..75c763a 100644
--- a/views_secondary_row.module
+++ b/views_secondary_row.module
@@ -1,5 +1,12 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\views_secondary_row\Hook\ViewsSecondaryRowHooks;
+
 /**
  * @file
  * Adds a new Views Row style.
@@ -11,16 +18,7 @@
  * Register views theming functions and those that are defined via views plugin
  * definitions.
  */
+#[LegacyHook]
 function views_secondary_row_theme($existing, $type, $theme, $path) {
-  $theme_info = [
-    'views_secondary_row_style_plugin_table' => [
-      'render element' => 'form',
-      'file' => 'views_secondary_row.theme.inc',
-    ],
-    'views_secondary_row_view_table' => [
-      'render element' => 'form',
-      'file' => 'views_secondary_row.theme.inc',
-    ],
-  ];
-  return $theme_info;
+  return \Drupal::service(ViewsSecondaryRowHooks::class)->theme($existing, $type, $theme, $path);
 }
diff --git a/views_secondary_row.services.yml b/views_secondary_row.services.yml
new file mode 100644
index 0000000..fbb14d4
--- /dev/null
+++ b/views_secondary_row.services.yml
@@ -0,0 +1,9 @@
+
+services:
+  Drupal\views_secondary_row\Hook\ViewsSecondaryRowHooks:
+    class: Drupal\views_secondary_row\Hook\ViewsSecondaryRowHooks
+    autowire: true
+
+  Drupal\views_secondary_row\Hook\ViewsSecondaryRowThemeHooks:
+    class: Drupal\views_secondary_row\Hook\ViewsSecondaryRowThemeHooks
+    autowire: true
diff --git a/views_secondary_row.theme.inc b/views_secondary_row.theme.inc
index 39dca60..42916b6 100644
--- a/views_secondary_row.theme.inc
+++ b/views_secondary_row.theme.inc
@@ -5,11 +5,8 @@
  * Preprocessors and helper functions to make theming easier.
  */
 
-use Drupal\Component\Utility\Html;
-use Drupal\Core\Render\Element;
-use Drupal\Core\Template\Attribute;
-use Drupal\Core\Url;
-use Drupal\Core\Utility\TableSort;
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\views_secondary_row\Hook\ViewsSecondaryRowThemeHooks;
 
 /**
  * Prepares variables for views table templates.
@@ -34,349 +31,9 @@ use Drupal\Core\Utility\TableSort;
  * Version history: Aligned with Views 9.4.x.
  * Version history: Aligned with Views 7.x-3.6, not changed in 3.13.
  */
+#[LegacyHook]
 function template_preprocess_views_secondary_row_view_table(&$variables) {
-  /** @var \Drupal\views\Plugin\views\style\Table $handler */
-  $view = $variables['view'];
-
-  // We need the raw data for this grouping, which is passed in
-  // as $variables['rows'].
-  // However, the template also needs to use for the rendered fields. We
-  // therefore swap the raw data out to a new variable
-  // and reset $variables['rows'] so that it can get rebuilt.
-  // Store rows so that they may be used by further preprocess functions.
-  $result = $variables['result'] = $variables['rows'];
-  $variables['rows'] = [];
-  $variables['header'] = [];
-
-  $options = $view->style_plugin->options;
-  $handler = $view->style_plugin;
-
-  $fields = &$view->field;
-  $columns = $handler->sanitizeColumns($options['columns'], $fields);
-
-  $active = !empty($handler->active) ? $handler->active : '';
-  $order = !empty($handler->order) ? $handler->order : 'asc';
-
-  // A boolean variable which stores whether the table has a responsive class.
-  $responsive = FALSE;
-
-  // Check if secondary rows are specified.
-  // If not, just call the default 'table' preprocessing.
-  $has_secondary_row = FALSE;
-  foreach ($columns as $field => $column) {
-    if (!empty($options['info'][$field]['break2'])) {
-      $has_secondary_row = TRUE;
-    }
-  }
-
-  $query = TableSort::getQueryParameters(\Drupal::request());
-  if (isset($view->exposed_raw_input)) {
-    $query += $view->exposed_raw_input;
-  }
-
-  // A boolean to store whether the table's header has any labels.
-  $has_header_labels = FALSE;
-  foreach ($columns as $field => $column) {
-    // Create a second variable so we can easily find what fields we have and
-    // what the CSS classes should be.
-    $variables['fields'][$field] = Html::cleanCssIdentifier($field);
-    if ($active == $field) {
-      $variables['fields'][$field] .= ' is-active';
-    }
-
-    // Render the header labels.
-    // Skip the header labels that are broken into the secondary row.
-    if ($field == $column && empty($fields[$field]->options['exclude']) && empty($options['info'][$field]['break2'])) {
-
-      $label = !empty($fields[$field]) ? $fields[$field]->label() : '';
-      if (empty($options['info'][$field]['sortable']) || !$fields[$field]->clickSortable()) {
-        $variables['header'][$field]['content'] = $label;
-      }
-      else {
-        $initial = !empty($options['info'][$field]['default_sort_order']) ? $options['info'][$field]['default_sort_order'] : 'asc';
-
-        if ($active == $field) {
-          $initial = ($order == 'asc') ? 'desc' : 'asc';
-        }
-
-        $title = t('sort by @s', [
-          '@s' => $label,
-        ]);
-        if ($active == $field) {
-          $variables['header'][$field]['sort_indicator'] = [
-            '#theme' => 'tablesort_indicator',
-            '#style' => $initial,
-          ];
-        }
-
-        $query['order'] = $field;
-        $query['sort'] = $initial;
-        $link_options = [
-          'query' => $query,
-        ];
-        // It is ok to specify no URL path here as we will always reload the
-        // current page.
-        $url = new Url('<none>', [], $link_options);
-        $variables['header'][$field]['url'] = $url->toString();
-        $variables['header'][$field]['content'] = $label;
-        $variables['header'][$field]['title'] = $title;
-      }
-
-      $variables['header'][$field]['default_classes'] = $fields[$field]->options['element_default_classes'];
-      // Set up the header label class.
-      $variables['header'][$field]['attributes'] = [];
-      $class = $fields[$field]->elementLabelClasses(0);
-      if ($class) {
-        $variables['header'][$field]['attributes']['class'][] = $class;
-      }
-      // Add responsive header classes.
-      if (!empty($options['info'][$field]['responsive'])) {
-        $variables['header'][$field]['attributes']['class'][] = $options['info'][$field]['responsive'];
-        $responsive = TRUE;
-      }
-      // Add a CSS align class to each field if one was set.
-      if (!empty($options['info'][$field]['align'])) {
-        $variables['header'][$field]['attributes']['class'][] = Html::cleanCssIdentifier($options['info'][$field]['align']);
-      }
-      // Add a header label wrapper if one was selected.
-      if ($variables['header'][$field]['content']) {
-        $element_label_type = $fields[$field]->elementLabelType(TRUE, TRUE);
-        if ($element_label_type) {
-          $variables['header'][$field]['wrapper_element'] = $element_label_type;
-        }
-        // Improves accessibility of complex tables.
-        $variables['header'][$field]['attributes']['id'] = Html::getUniqueId('view-' . $field . '-table-column');
-      }
-      // Check if header label is not empty.
-      if (!empty($variables['header'][$field]['content'])) {
-        $has_header_labels = TRUE;
-      }
-
-      $variables['header'][$field]['attributes'] = new Attribute($variables['header'][$field]['attributes']);
-    }
-
-    // Add a CSS align class to each field if one was set.
-    if (!empty($options['info'][$field]['align'])) {
-      $variables['fields'][$field] .= ' ' . Html::cleanCssIdentifier($options['info'][$field]['align']);
-    }
-
-    // Render each field into its appropriate column.
-    foreach ($result as $num => $row) {
-
-      // Skip building the attributes and content if the field is to be excluded
-      // from the display.
-      if (!empty($fields[$field]->options['exclude'])) {
-        continue;
-      }
-
-      // Make all rows even rows,
-      // so that there's an empty odd/secondary row between them,
-      // where the "secondary row" fields can be placed.
-      $old_num = $num;
-      $primary_row_num = $num * 2;
-      $secondary_row_num = $primary_row_num + 1;
-      $num = $primary_row_num;
-      if (!empty($options['info'][$field]['break2'])) {
-        $num = $secondary_row_num;
-        $column = $options['info'][$field]['break2'];
-      }
-
-      // Reference to the column in the loop to make the code easier to read.
-      $column_reference =& $variables['rows'][$num]['columns'][$column];
-
-      $column_reference['default_classes'] = $fields[$field]->options['element_default_classes'];
-
-      // Set the field key to the column so it can be used for adding classes
-      // in a template.
-      $column_reference['fields'][] = $variables['fields'][$field];
-
-      // Add field classes.
-      if (!isset($column_reference['attributes'])) {
-        $column_reference['attributes'] = [];
-      }
-
-      if ($classes = $fields[$field]->elementClasses($num)) {
-        $column_reference['attributes']['class'][] = $classes;
-      }
-
-      // Add rowspan to primary row if it is configured as 2.
-      if ($num === $primary_row_num && isset($options['info'][$field]['rowspan1']) && (int) $options['info'][$field]['rowspan1'] === 2) {
-        $column_reference['attributes']['rowspan'][] = 2;
-      }
-
-      // Add colspan to secondary row.
-      if ($num == $secondary_row_num && !empty($options['info'][$field]['break2']) && !empty($options['info'][$field]['colspan2'])) {
-        $column_reference['attributes']['colspan'][] = $options['info'][$field]['colspan2'];
-      }
-
-      // Add responsive header classes.
-      if (!empty($options['info'][$field]['responsive'])) {
-        $column_reference['attributes']['class'][] = $options['info'][$field]['responsive'];
-      }
-
-      // Improves accessibility of complex tables.
-      if (isset($variables['header'][$field]['attributes']['id'])) {
-        $column_reference['attributes']['headers'] = [$variables['header'][$field]['attributes']['id']];
-      }
-
-      if (!empty($fields[$field])) {
-        // Modification for secondary row. Use $old_num, else empty rows appear.
-        $field_output = $handler->getField($old_num, $field);
-        $column_reference['wrapper_element'] = $fields[$field]->elementType(TRUE, TRUE);
-        if (!isset($column_reference['content'])) {
-          $column_reference['content'] = [];
-        }
-
-        // Only bother with separators and stuff if the field shows up.
-        // Place the field into the column, along with an optional separator.
-        if (trim($field_output) != '') {
-          if ($num == $secondary_row_num && !empty($options['info'][$field]['break2'])) {
-            if (!empty($options['info'][$column]['separator2'])) {
-              $column_reference['content'][] = [
-                'separator' => ['#markup' => $options['info'][$column]['separator2']],
-                'field_output' => ['#markup' => $field_output],
-              ];
-            }
-            else {
-              $column_reference['content'][] = [
-                'field_output' => ['#markup' => $field_output],
-              ];
-            }
-          }
-          elseif (!empty($column_reference['content']) && !empty($options['info'][$column]['separator'])) {
-            $column_reference['content'][] = [
-              'separator' => ['#markup' => $options['info'][$column]['separator']],
-              'field_output' => ['#markup' => $field_output],
-            ];
-          }
-          else {
-            $column_reference['content'][] = [
-              'field_output' => ['#markup' => $field_output],
-            ];
-          }
-        }
-      }
-      $column_reference['attributes'] = new Attribute($column_reference['attributes']);
-    }
-
-    // Remove columns if the "empty_column" option is checked and the
-    // field is empty.
-    if (!empty($options['info'][$field]['empty_column'])) {
-      $empty = TRUE;
-      foreach ($variables['rows'] as $columns) {
-        $empty &= empty($columns['columns'][$column]['content']);
-      }
-      if ($empty) {
-        foreach ($variables['rows'] as &$column_items) {
-          unset($column_items['columns'][$column]);
-        }
-        unset($variables['header'][$column]);
-      }
-    }
-  }
-
-  // Reorder rows, since odd/secondary rows are appended at the end of the list.
-  ksort($variables['rows']);
-  // Reorder columns.
-  // Sort order of fields in secondary rows may differ from target columns.
-  foreach ($variables['rows'] as $num => $row) {
-    if ($num % 2 == 0) {
-      continue;
-    }
-    $ordered = [];
-    foreach ($columns as $field => $column) {
-      if (isset($row['columns'][$column])) {
-        $ordered['columns'][$column] = $row['columns'][$column];
-      }
-    }
-    $variables['rows'][$num]['columns'] = $ordered['columns'];
-  }
-
-  // Hide table header if all labels are empty.
-  if (!$has_header_labels) {
-    $variables['header'] = [];
-  }
-
-  foreach ($variables['rows'] as $num => $row) {
-    $variables['rows'][$num]['attributes'] = [];
-    if ($row_class = $handler->getRowClass($num)) {
-      $variables['rows'][$num]['attributes']['class'][] = $row_class;
-    }
-    $emptyRow = TRUE;
-    foreach ($row['columns'] as $field => $column) {
-      if (!empty($column['content'])) {
-        $emptyRow = FALSE;
-        continue;
-      }
-    }
-    if ($emptyRow) {
-      $variables['rows'][$num]['attributes']['class'][] = 'views-secondary-row--no-content hidden';
-    }
-    $variables['rows'][$num]['attributes'] = new Attribute($variables['rows'][$num]['attributes']);
-  }
-
-  if (empty($variables['rows']) && !empty($options['empty_table'])) {
-    $build = $view->display_handler->renderArea('empty');
-    $variables['rows'][0]['columns'][0]['content'][0]['field_output'] = $build;
-    $variables['rows'][0]['attributes'] = new Attribute(['class' => ['odd']]);
-    // Calculate the amounts of rows with output.
-    $variables['rows'][0]['columns'][0]['attributes'] = new Attribute([
-      'colspan' => count($variables['header']),
-      'class' => ['views-empty'],
-    ]);
-  }
-
-  $variables['sticky'] = FALSE;
-  if (!empty($options['sticky'])) {
-    $variables['view']->element['#attached']['library'][] = 'core/drupal.tableheader';
-    $variables['sticky'] = TRUE;
-  }
-
-  // Add the caption to the list if set.
-  if (!empty($handler->options['caption'])) {
-    $variables['caption'] = ['#markup' => $handler->options['caption']];
-    $variables['caption_needed'] = TRUE;
-  }
-  elseif (!empty($variables['title'])) {
-    $variables['caption'] = ['#markup' => $variables['title']];
-    $variables['caption_needed'] = TRUE;
-  }
-  else {
-    $variables['caption'] = '';
-    $variables['caption_needed'] = FALSE;
-  }
-
-  // For backwards compatibility, initialize the 'summary' and 'description'
-  // variables, although core templates now all use 'summary_element' instead.
-  $variables['summary'] = $handler->options['summary'];
-  $variables['description'] = $handler->options['description'];
-  if (!empty($handler->options['summary']) || !empty($handler->options['description'])) {
-    $variables['summary_element'] = [
-      '#type' => 'details',
-      '#title' => $handler->options['summary'],
-      // To ensure that the description is properly escaped during rendering,
-      // use an 'inline_template' to let Twig do its magic, instead of 'markup'.
-      'description' => [
-        '#type' => 'inline_template',
-        '#template' => '{{ description }}',
-        '#context' => [
-          'description' => $handler->options['description'],
-        ],
-      ],
-    ];
-    $variables['caption_needed'] = TRUE;
-  }
-
-  $variables['responsive'] = FALSE;
-  // If the table has headers and it should react responsively to columns hidden
-  // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM
-  // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors.
-  if (isset($variables['header']) && $responsive) {
-    $variables['view']->element['#attached']['library'][] = 'core/drupal.tableresponsive';
-    // Add 'responsive-enabled' class to the table to identify it for JS.
-    // This is needed to target tables constructed by this function.
-    $variables['responsive'] = TRUE;
-  }
+  \Drupal::service(ViewsSecondaryRowThemeHooks::class)->templatePreprocessViewsSecondaryRowViewTable($variables);
 }
 
 /**
@@ -391,122 +48,7 @@ function template_preprocess_views_secondary_row_view_table(&$variables) {
  *
  * Default template: views-ui-style-plugin-table.html.twig.
  */
+#[LegacyHook]
 function template_preprocess_views_secondary_row_style_plugin_table(&$variables) {
-  // @todo First, call the default views_ui function, then change
-  // template_preprocess_views_ui_style_plugin_table($variables).
-  $form = $variables['form'];
-  $header = [
-    t('Field'),
-    t('Column'),
-    t('Align'),
-    t('Separator'),
-    /* Begin insert views_secondary_row. */
-    [
-      'data' => t('1st row Rowspan'),
-      'align' => 'center',
-    ],
-    [
-      'data' => t('Secondary row'),
-      'align' => 'center',
-    ],
-    [
-      'data' => t('2nd row Separator'),
-      'align' => 'center',
-    ],
-    [
-      'data' => t('2nd row Colspan'),
-      'align' => 'center',
-    ],
-    /* End insert views_secondary_row. */
-    [
-      'data' => t('Sortable'),
-      'align' => 'center',
-    ],
-    [
-      'data' => t('Default order'),
-      'align' => 'center',
-    ],
-    [
-      'data' => t('Default sort'),
-      'align' => 'center',
-    ],
-    [
-      'data' => t('Hide empty column'),
-      'align' => 'center',
-    ],
-    [
-      'data' => t('Responsive'),
-      'align' => 'center',
-    ],
-  ];
-  $rows = [];
-  foreach (Element::children($form['columns']) as $id) {
-    $row = [];
-    $row[]['data'] = $form['info'][$id]['name'];
-    $row[]['data'] = $form['columns'][$id];
-    $row[]['data'] = $form['info'][$id]['align'];
-    $row[]['data'] = $form['info'][$id]['separator'];
-    /* Begin insert views_secondary_row. */
-    $row[]['data'] = $form['info'][$id]['rowspan1'];
-    $row[]['data'] = $form['info'][$id]['break2'];
-    $row[]['data'] = $form['info'][$id]['separator2'];
-    $row[]['data'] = $form['info'][$id]['colspan2'];
-    /* End insert views_secondary_row. */
-    if (!empty($form['info'][$id]['sortable'])) {
-      $row[] = [
-        'data' => $form['info'][$id]['sortable'],
-        'align' => 'center',
-      ];
-      $row[] = [
-        'data' => $form['info'][$id]['default_sort_order'],
-        'align' => 'center',
-      ];
-      $row[] = [
-        'data' => $form['default'][$id],
-        'align' => 'center',
-      ];
-    }
-    else {
-      $row[] = '';
-    }
-    $row[] = [
-      'data' => $form['info'][$id]['empty_column'],
-      'align' => 'center',
-    ];
-    $row[] = [
-      'data' => $form['info'][$id]['responsive'],
-      'align' => 'center',
-    ];
-    $rows[] = $row;
-  }
-
-  // Add the special 'None' row.
-  $rows[] = [
-    [
-      'data' => t('None'),
-      /* Begin replace views_secondary_row colspan from 6 to new value. */
-      'colspan' => 10,
-      /* End replace views_secondary_row colspan from 6 to new value. */
-    ],
-    [
-      'align' => 'center',
-      'data' => $form['default'][-1],
-    ],
-    [
-      'colspan' => 2,
-    ],
-  ];
-
-  // Unset elements from the form array that are used to build the table so that
-  // they are not rendered twice.
-  unset($form['default']);
-  unset($form['info']);
-  unset($form['columns']);
-  $variables['table'] = [
-    '#type' => 'table',
-    '#theme' => 'table__views_ui_style_plugin_table',
-    '#header' => $header,
-    '#rows' => $rows,
-  ];
-  $variables['form'] = $form;
+  \Drupal::service(ViewsSecondaryRowThemeHooks::class)->templatePreprocessViewsSecondaryRowStylePluginTable($variables);
 }
