<?php

/**
 * @file
 * Theme hooks, preprocessor, and suggestions.
 */

use Drupal\Core\Render\Element;
use Drupal\file\Entity\File;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Template\Attribute;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\webform\Element\WebformSelectOther;
use Drupal\webform\Plugin\WebformElement\Radios as WebformRadios;
use Drupal\webform\Plugin\WebformElement\WebformEntityRadios;
use Drupal\webform\Utility\WebformAccessibilityHelper;
use Drupal\webform\Utility\WebformElementHelper;

/* ************************************************************************** */
// Theme hooks.
/* ************************************************************************** */

/**
 * Implements hook_theme().
 */
function webform_theme() {
  $info = [
    'webform_help' => [
      'variables' => ['info' => []],
    ],
    'webform_help_video_youtube' => [
      'variables' => ['youtube_id' => NULL, 'autoplay' => TRUE],
    ],
    'webform_help_support' => [
      'variables' => [
        'account' => [],
        'membership' => [],
        'contribution' => [],
      ],
    ],
    'webform' => [
      'render element' => 'element',
    ],
    'webform_actions' => [
      'render element' => 'element',
    ],
    'webform_access_denied' => [
      'variables' => ['message' => '', 'attributes' => [], 'webform' => NULL],
    ],

    'webform_handler_action_summary' => [
      'variables' => ['settings' => NULL, 'handler' => NULL],
    ],
    'webform_handler_debug_summary' => [
      'variables' => ['settings' => NULL, 'handler' => NULL],
    ],
    'webform_handler_email_summary' => [
      'variables' => ['settings' => NULL, 'handler' => NULL],
    ],
    'webform_handler_remote_post_summary' => [
      'variables' => ['settings' => NULL, 'handler' => NULL],
    ],
    'webform_handler_settings_summary' => [
      'variables' => ['settings' => NULL, 'handler' => NULL],
    ],

    'webform_variant_override_summary' => [
      'variables' => ['settings' => NULL, 'variant' => NULL],
    ],

    'webform_confirmation' => [
      'variables' => ['webform' => NULL, 'source_entity' => NULL, 'webform_submission' => NULL],
    ],

    'webform_required' => [
      'variables' => ['label' => NULL],
    ],

    'webform_submission' => [
      'render element' => 'elements',
    ],

    'webform_submission_form' => [
      'render element' => 'form',
    ],

    'webform_submission_navigation' => [
      'variables' => ['webform_submission' => NULL],
    ],
    'webform_submission_information' => [
      'variables' => ['webform_submission' => NULL, 'source_entity' => NULL, 'open' => TRUE],
    ],
    'webform_submission_data' => [
      'render element' => 'elements',
    ],

    'webform_element_base_html' => [
      'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => []],
    ],
    'webform_element_base_text' => [
      'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => []],
    ],

    'webform_container_base_html' => [
      'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => []],
    ],
    'webform_container_base_text' => [
      'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => []],
    ],

    'webform_element_help' => [
      'variables' => ['help' => NULL, 'help_title' => '', 'attributes' => []],
    ],

    'webform_element_more' => [
      'variables' => ['more' => NULL, 'more_title' => '', 'attributes' => []],
    ],

    'webform_element_managed_file' => [
      'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => [], 'file' => NULL],
    ],
    'webform_element_audio_file' => [
      'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => [], 'file' => NULL],
    ],
    'webform_element_document_file' => [
      'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => [], 'file' => NULL],
    ],
    'webform_element_image_file' => [
      'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => [], 'file' => NULL, 'style_name' => NULL, 'format' => NULL],
    ],
    'webform_element_video_file' => [
      'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => [], 'file' => NULL],
    ],

    'webform_email_html' => [
      'variables' => ['subject' => '', 'body' => '', 'webform_submission' => NULL, 'handler' => NULL],
    ],
    'webform_email_message_html' => [
      'variables' => ['message' => '', 'webform_submission' => NULL, 'handler' => NULL],
    ],
    'webform_email_message_text' => [
      'variables' => ['message' => '', 'webform_submission' => NULL, 'handler' => NULL],
    ],

    'webform_html_editor_markup' => [
      'variables' => ['markup' => NULL, 'allowed_tags' => []],
    ],
    'webform_horizontal_rule' => [
      'render element' => 'element',
    ],
    'webform_message' => [
      'render element' => 'element',
    ],
    'webform_section' => [
      'render element' => 'element',
    ],

    'webform_composite_address' => [
      'render element' => 'element',
    ],
    'webform_composite_contact' => [
      'render element' => 'element',
    ],
    'webform_composite_location' => [
      'render element' => 'element',
    ],
    'webform_composite_link' => [
      'render element' => 'element',
    ],
    'webform_composite_name' => [
      'render element' => 'element',
    ],
    'webform_composite_telephone' => [
      'render element' => 'element',
    ],

    'webform_codemirror' => [
      'variables' => ['code' => NULL, 'type' => 'text'],
    ],

    'webform_progress' => [
      'variables' => [
        'webform' => NULL,
        'webform_submission' => NULL,
        'current_page' => NULL,
        'operation' => NULL,
        'pages' => [],
      ],
    ],

    'webform_progress_bar' => [
      'variables' => [
        'webform' => NULL,
        'webform_submission' => NULL,
        'current_page' => NULL,
        'operation' => NULL,
        'max_pages' => 10,
        'pages' => [],
      ],
    ],

    'webform_progress_tracker' => [
      'variables' => [
        'webform' => NULL,
        'webform_submission' => NULL,
        'current_page' => NULL,
        'operation' => NULL,
        'max_pages' => 10,
        'pages' => [],
      ],
    ],
  ];

  // Since any rendering of a webform is going to require 'webform.theme.inc'
  // we are going to just add it to every template.
  foreach ($info as &$template) {
    $template['file'] = 'includes/webform.theme.template.inc';
  }

  return $info;
}

/**
 * Implements hook_theme_registry_alter().
 */
function webform_theme_registry_alter(&$theme_registry) {
  // Allow attributes to be defined for status messages so that #states
  // can be added to messages.
  // @see \Drupal\webform\Element\WebformMessage
  if (!isset($theme_registry['status_messages']['variables']['attributes'])) {
    $theme_registry['status_messages']['variables']['attributes'] = [];
  }
}

/* ************************************************************************** */
// Preprocessors.
/* ************************************************************************** */

/**
 * Implements preprocess_menu_local_action() for single local action link templates.
 *
 * Applies custom link attributes to local actions.
 * Custom attributes are used to open Webform UI modals.
 *
 * @see template_preprocess_menu_local_action();
 * @see \Drupal\webform\WebformEntityHandlersForm
 * @see \Drupal\webform_ui\WebformUiEntityEditForm
 * @see https://www.drupal.org/node/2897396
 */
function webform_preprocess_menu_local_action(&$variables) {
  $link = $variables['element']['#link'];
  // Only need to update local actions with link attributes.
  if (!isset($link['attributes'])) {
    return;
  }

  $link += [
    'localized_options' => [],
  ];
  $link['localized_options']['attributes'] = $link['attributes'];
  $link['localized_options']['attributes']['class'][] = 'button';
  $link['localized_options']['attributes']['class'][] = 'button-action';
  $link['localized_options']['set_active_class'] = TRUE;

  $variables['link'] = [
    '#type' => 'link',
    '#title' => $link['title'],
    '#options' => $link['localized_options'],
    '#url' => $link['url'],
  ];
}

/**
 * Implements hook_preprocess_checkboxes() for checkboxes templates.
 *
 * @see \Drupal\webform\Plugin\WebformElement\OptionsBase
 */
function webform_preprocess_checkboxes(&$variables) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return;
  }

  _webform_preprocess_options($variables);
}

/**
 * Implements hook_preprocess_radios() for radios templates.
 *
 * @see \Drupal\webform\Plugin\WebformElement\OptionsBase
 */
function webform_preprocess_radios(&$variables) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return;
  }

  _webform_preprocess_options($variables);
}

/**
 * Implements hook_preprocess_select() for select templates.
 */
function webform_preprocess_select(&$variables) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return;
  }

  $element = $variables['element'];

  // When options are sorted (viu #sort_options: true) make sure the
  // select '_other_' options is always last.
  // @see \Drupal\webform\Element\WebformOtherBase::processWebformOther
  // @see template_preprocess_select().
  if (!empty($element['#sort_options']) && !empty($element['#webform_other'])) {
    $options = &$variables['options'];
    foreach ($options as $index => $option) {
      if ($option['value'] === WebformSelectOther::OTHER_OPTION) {
        unset($options[$index]);
        $options[] = $option;
        $options = array_values($options);
        break;
      }
    }
  }
}

/**
 * Prepares variable for status_messages.
 */
function webform_preprocess_status_messages(&$variables) {
  if (!isset($variables['status_headings']['info'])) {
    $variables['status_headings']['info'] = t('Information message');
  }
}

/* ************************************************************************** */
// Preprocess tables.
/* ************************************************************************** */

/**
 * Implements hook_preprocess_table() for table templates.
 */
function webform_preprocess_table(&$variables) {
  // Add links to 'Translate' webform tab.
  if (\Drupal::routeMatch()->getRouteName() === 'entity.webform.config_translation_overview') {
    /** @var \Drupal\webform\WebformInterface $webform */
    $webform = \Drupal::routeMatch()->getParameter('webform');
    foreach ($variables['rows'] as &$row) {
      // Check first cell.
      if (!isset($row['cells'][0]['content'])
        || !is_array($row['cells'][0]['content'])
        || !isset($row['cells'][0]['content']['#markup'])) {
        continue;
      }

      // Check last cell edit link.
      if (!isset($row['cells'][1]['content'])
        || !is_array($row['cells'][1]['content'])
        || !isset($row['cells'][1]['content']['#links'])
        || !is_array($row['cells'][1]['content']['#links'])
        || !isset($row['cells'][1]['content']['#links']['edit'])) {
        continue;
      }

      // Get language from edit link.
      $route_parameters = $row['cells'][1]['content']['#links']['edit']['url']->getRouteParameters();
      $langcode = $route_parameters['langcode'] ?? NULL;
      $language = \Drupal::languageManager()->getLanguage($langcode);

      // Convert the first cell in the row to a link.
      $row['cells'][0]['content'] = [
        '#type' => 'link',
        '#url' => $webform->toUrl('canonical', ['language' => $language]),
        '#title' => $row['cells'][0]['content'],
      ];
    }
  }
}

/* ************************************************************************** */
// Preprocess containers.
/* ************************************************************************** */

/**
 * Implements hook_preprocess_datetime_form() for datetime form element templates.
 */
function webform_preprocess_datetime_form(&$variables) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return;
  }

  $element = $variables['element'];

  // Date and time custom placeholder.
  if (isset($element['#date_date_placeholder']) && isset($variables['content']['date'])) {
    $variables['content']['date']['#attributes']['placeholder'] = $element['#date_date_placeholder'];
  }
  if (isset($element['#date_time_placeholder']) && isset($variables['content']['time'])) {
    $variables['content']['time']['#attributes']['placeholder'] = $element['#date_time_placeholder'];
  }

  // Add .container-inline to datetime form wrapper which is missing from the
  // stable base theme.
  // @see core/themes/classy/templates/form/datetime-form.html.twig
  // @see core/themes/stable/templates/form/datetime-form.html.twig
  $variables['attributes']['class'][] = 'container-inline';
}

/**
 * Implements hook_preprocess_details() for details element templates.
 */
function webform_preprocess_details(&$variables) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return;
  }

  // Setup description, help, and more.
  _webform_preprocess_element($variables);

  $element = &$variables['element'];

  // Hide details title.
  if (isset($element['#title_display']) && $element['#title_display'] === 'invisible') {
    $variables['title'] = WebformAccessibilityHelper::buildVisuallyHidden($variables['title']);
  }

  // Remove invalid 'required' and 'aria-required' attributes from details.
  if (isset($element['#webform_key'])) {
    unset(
      $variables['attributes']['required'],
      $variables['attributes']['aria-required']
    );
  }
}

/**
 * Implements hook_preprocess_fieldset() for fieldset templates.
 */
function webform_preprocess_fieldset(&$variables) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return;
  }

  // Setup description, help, and more.
  _webform_preprocess_element($variables, ['legend', 'title']);

  $element = &$variables['element'];

  // Apply inline title defined by radios, checkboxes, and buttons.
  // @see \Drupal\webform\Plugin\WebformElement\OptionsBase::prepare
  if (isset($element['#_title_display'])) {
    $variables['attributes']['class'][] = 'webform-fieldset--title-inline';
  }

  // If the title display is none remove the legend.title and set
  // display to none.
  if (isset($element['#title_display']) && $element['#title_display'] === 'none') {
    $variables['legend'] = ['attributes' => new Attribute(['style' => 'display:none'])];
  }

  // Add .js-webform-form-type-* class to be used JavaScript and #states API.
  // @see js/webform.element.location.geocomplete.js
  // @see js/webform.states.js
  if (isset($element['#type'])) {
    $variables['attributes']['class'][] = 'js-webform-type-' . Html::getClass($element['#type']);
    $variables['attributes']['class'][] = 'webform-type-' . Html::getClass($element['#type']);
  }

  // Remove invalid 'required' attributes from fieldset.
  //
  // Issue #3174459: W3C Validation: required attribute not allowed on
  // fieldset tag.
  // @see https://www.drupal.org/project/drupal/issues/3174459
  if (isset($element['#webform_key'])) {
    unset($variables['attributes']['required']);
  }

  // Add aria-required="true" only on fieldsets containing required
  // radiobuttons.
  //
  // Issue #3240249: Aria-required on fieldset trigger accessibility fails.
  // @see https://www.drupal.org/project/webform/issues/3240249
  // @see https://www.drupal.org/project/webform/issues/3277192
  /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
  $element_manager = \Drupal::service('plugin.manager.webform.element');
  $element_plugin = $element_manager->getElementInstance($element);
  $is_radios = ($element_plugin instanceof WebformRadios || $element_plugin instanceof WebformEntityRadios);
  if ($is_radios) {
    $id = $variables['attributes']['id'] . '-legend';
    $variables['legend']['attributes']['id'] = $id;

    if (isset($variables['attributes']['aria-required'])) {
      // The aria-required is _not_ necessary on a fieldset of radio elements.
      unset($variables['attributes']['aria-required']);
    }
  }
  elseif (isset($variables['attributes']['aria-required'])) {
    unset($variables['attributes']['aria-required']);
  }
}

/* ************************************************************************** */
// Preprocess form element.
/* ************************************************************************** */

/**
 * Implements hook_preprocess_form_element() for form element templates.
 */
function webform_preprocess_form_element(&$variables) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return;
  }

  // Setup description, help, and more.
  _webform_preprocess_element($variables, ['label', '#title']);

  // Make sure the #description_display is always applied to account for
  // #more which is placed in the #description.
  // @see template_preprocess_form_element()
  if (isset($variables['description'])) {
    $element = &$variables['element'];
    $variables['description_display'] = $element['#description_display'];
  }

  // Issue #2700439: Description class not added to
  // form-element.html.twig template.
  // @see https://www.drupal.org/project/drupal/issues/2700439
  if (isset($variables['description_display']) && $variables['description_display'] === 'before') {
    // Add system .description class.
    if (isset($variables['description'])) {
      if (empty($variables['description']['attributes'])) {
        $variables['description']['attributes'] = new Attribute();
      }
      $variables['description']['attributes']->addClass('description');
    }
  }

  // Add missing classes to the Claro theme's form elements.
  // @see core/modules/system/templates/form-element.html.twig
  // @see claro/templates/form-element.html.twig
  // @todo Once Claro and Oliver are stable determine if this code is needed.
  static $is_claro_theme;
  static $is_olivero_theme;
  if (!isset($is_claro_theme)) {
    /** @var \Drupal\webform\WebformThemeManagerInterface $theme_manager */
    $theme_manager = \Drupal::service('webform.theme_manager');
    $is_claro_theme = $theme_manager->isActiveTheme('claro');
    $is_olivero_theme = $theme_manager->isActiveTheme('olivero');
  }
  if ($is_claro_theme || $is_olivero_theme) {
    // Add system .form-type-TYPE class.
    if (!empty($variables['type'])) {
      $variables['attributes']['class'][] = 'form-type-' . Html::getClass($variables['type']);
    }

    // Add system .description class.
    if (isset($variables['description'])) {
      if (empty($variables['description']['attributes'])) {
        $variables['description']['attributes'] = new Attribute();
      }
      $variables['description']['attributes']->addClass('description');
    }
  }
}

/**
 * Implements hook_preprocess_form_element_label() for form element label templates.
 */
function webform_preprocess_form_element_label(&$variables) {
  $element = &$variables['element'];

  // Fix variable title #markup tha contains a render array which is most
  // likely a Help tooltip.
  // @see template_preprocess_form_element_label()
  // @see webform_preprocess_form_element()
  if (isset($variables['title'])
    && is_array($variables['title'])
    && is_array($variables['title']['#markup'])) {
    $variables['title'] = $variables['title']['#markup'];
  }

  // Remove label 'for' attribute.
  if (!empty($element['#attributes']['webform-remove-for-attribute'])) {
    unset($element['#attributes']['webform-remove-for-attribute']);
    unset($variables['attributes']['webform-remove-for-attribute']);
    unset($variables['attributes']['for']);
  }
}

/* ************************************************************************** */
// Preprocess file/image elements.
/* ************************************************************************** */

/**
 * Implements hook_preprocess_file_managed_file() for file managed file templates.
 *
 * @see https://stackoverflow.com/questions/21842274/cross-browser-custom-styling-for-file-upload-button
 * @see template_preprocess_file_managed_file()
 */
function webform_preprocess_file_managed_file(&$variables) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return;
  }

  $element = &$variables['element'];
  if (empty($element['#button'])) {
    return;
  }

  // Don't alter hidden file upload input.
  if (!Element::isVisibleElement($element['upload'])) {
    return;
  }

  // Create an unique id for the file upload input and label.
  $button_id = Html::getUniqueId($variables['element']['upload']['#id'] . '-button');

  // Create a label that is styled like an action button.
  $label = [
    '#type' => 'html_tag',
    '#tag' => 'label',
    '#value' => $element['#button__title'] ?? (empty($element['#multiple']) ? t('Choose file') : t('Choose files')),
    '#attributes' => $element['#button__attributes'] ?? [],
  ];

  // Add 'for' attribute.
  $label['#attributes']['for'] = $button_id;

  // Add default button classes.
  if (empty($label['#attributes']['class'])) {
    $label['#attributes']['class'][] = 'button';
    $label['#attributes']['class'][] = 'button-action';
  }

  // Add .webform-file-button.
  $label['#attributes']['class'][] = 'webform-file-button';

  // Make sure the label is first.
  $element = ['label' => $label] + $element;

  // Set the custom button ID for file upload input.
  $element['upload']['#attributes']['id'] = $button_id;

  // Hide the file upload.
  $element['upload']['#attributes']['class'][] = 'webform-file-button-input';

  // Attach library.
  $element['#attached']['library'][] = 'webform/webform.element.file.button';
}

/**
 * Implements hook_preprocess_file_upload_help() for file upload help text templates.
 *
 * @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::prepare
 */
function webform_preprocess_file_upload_help(&$variables) {
  $upload_validators = $variables['upload_validators'];

  if (isset($upload_validators['webform_file_limit']) && $upload_validators['webform_file_limit'][0]) {
    $variables['descriptions'][] = t('@size limit per form.', ['@size' => format_size($upload_validators['webform_file_limit'][0])]);
  }

  // Issue #3136578: Comma-separate the list of allowed file extensions.
  // @see https://www.drupal.org/project/drupal/issues/3136578
  if (isset($upload_validators['webform_file_validate_extensions'])) {
    foreach ($variables['descriptions'] as $index => $description) {
      if ($description instanceof TranslatableMarkup
        && $description->getUntranslatedString() === 'Allowed types: @extensions.') {
        $arguments = $description->getArguments();
        if (is_string($arguments['@extensions']) && strpos($arguments['@extensions'], ',') === FALSE) {
          $arguments['@extensions'] = preg_replace('/ +/', ', ', $arguments['@extensions']);
          $variables['descriptions'][$index] = new TranslatableMarkup($description->getUntranslatedString(), $arguments);
        }
      }
    }
  }
}

/**
 * Implements hook_preprocess_file_link() for file link templates.
 *
 * @see webform_file_access
 */
function webform_preprocess_file_link(&$variables) {
  /** @var \Drupal\file\FileInterface $file */
  $file = $variables['file'];
  $file = ($file instanceof File) ? $file : File::load($file->fid);

  // Remove link to temporary anonymous private file uploads.
  if ($file->isTemporary() && $file->getOwner() && $file->getOwner()->isAnonymous() && strpos($file->getFileUri(), 'private://webform/') === 0) {
    $variables['link'] = $file->getFilename();
  }

  // Add file size variable to Claro theme.
  if (empty($variables['file_size'])
    && $file->getSize()
    && \Drupal::service('webform.theme_manager')->isActiveTheme('claro')) {
    $variables['file_size'] = format_size($file->getSize(), \Drupal::languageManager()->getCurrentLanguage()->getId());
  }
}

/**
 * Implements hook_preprocess_image() for image templates.
 *
 * Make sure the image src for the 'webform_image_file' src is an absolute URL.
 */
function webform_preprocess_image(&$variables) {
  global $base_url;
  if (isset($variables['attributes']['class'])
    && in_array('webform-image-file', (array) $variables['attributes']['class'])
    && !preg_match('#^https?://#', $variables['attributes']['src'])) {
    $variables['attributes']['src'] = $base_url . preg_replace('/^' . preg_quote(base_path(), '/') . '/', '/', $variables['attributes']['src']);
  }
}

/* ************************************************************************** */
// Preprocess helpers.
/* ************************************************************************** */

/**
 * Prepares variables for checkboxes and radios options templates.
 *
 * Below code must be called by template_preprocess_(radios|checkboxes) which
 * reset the element's 'attributes';
 */
function _webform_preprocess_options(array &$variables) {
  $element = &$variables['element'];

  $variables['attributes']['class'][] = Html::getClass('js-webform-' . $element['#type']);

  if (!empty($element['#options_display'])) {
    if (strpos($element['#options_display'], 'buttons_') === 0) {
      $variables['attributes']['class'][] = 'webform-options-display-buttons';
    }
    $variables['attributes']['class'][] = Html::getClass('webform-options-display-' . $element['#options_display']);
    $variables['#attached']['library'][] = 'webform/webform.element.options';
  }
}

/**
 * Prepares webform element description, help, and more templates.
 *
 * @see template_preprocess_form_element()
 * @see core/modules/system/templates/form-element.html.twig
 * @see template_preprocess_details()
 * @see /core/modules/system/templates/details.html.twig
 * @see template_preprocess_fieldset()
 * @see /core/modules/system/templates/fieldset.html.twig
 * @see template_preprocess_webform_section()
 * @see /webform/templates/webform-section.html.twig
 */
function _webform_preprocess_element(array &$variables, $title_parents = ['title']) {
  $element = &$variables['element'];
  $type = $element['#type'] ?? '';

  // Fix details 'description' property which does not have description.content.
  // @see template_preprocess_details
  // @see Issue #2896169: Details elements have incorrect aria-describedby attributes
  if (!empty($element['#description'])) {
    // Normalize description into a simple render array.
    if (is_array($element['#description'])) {
      $description = [$element['#description']];
    }
    else {
      $description = ['#markup' => $element['#description']];
    }

    if ($type === 'details') {
      $description_attributes = [];
      if (!empty($element['#id'])) {
        $description_attributes['id'] = $element['#id'] . '--description';
      }
      $variables['description'] = [];
      $variables['description']['content'] = [
        '#type' => 'container',
        '#attributes' => new Attribute($description_attributes),
      ] + $description;
    }
    else {
      $variables['description'] += ['attributes' => new Attribute()];
      // Wrap description in a container.
      $variables['description']['content'] = [
        '#type' => 'container',
        '#attributes' => $variables['description']['attributes'],
      ] + $description;
      $variables['description']['attributes'] = new Attribute();
    }

    $variables['description']['content']['#attributes']->addClass('webform-element-description');

    // Handle invisible descriptions.
    if (isset($element['#description_display']) && $element['#description_display'] === 'invisible') {
      $variables['description']['content']['#attributes']->addClass('visually-hidden');
      $variables['description_display'] = 'after';
    }

    // Nest description content so that we can a more link
    // below the description.
    $variables['description']['content'] = [
      'description' => $variables['description']['content'],
    ];
  }
  elseif (isset($variables['description']) && empty($variables['description'])) {
    // Unset $variable['description'] which can be set to NULL or empty string.
    // This allows $variable['description'] to be converted to render array.
    // @see template_preprocess_details()
    // @see template_preprocess_form_element()
    unset($variables['description']);
  }

  // Move #description to #help for webform admin routes.
  _webform_preprocess_description_help($variables);

  // Add (read) more to #description.
  _webform_preprocess_form_element_description_more($variables);

  // Add help to title (aka label).
  _webform_preprocess_help($variables, $title_parents);

  // Hide 'item' label[for].
  if ($type === 'item') {
    $variables['label']['#attributes']['webform-remove-for-attribute'] = TRUE;
  }
}

/**
 * Prepares #description and #help properties for form element templates.
 */
function _webform_preprocess_description_help(array &$variables) {
  /** @var \Drupal\webform\WebformRequestInterface $webform_request */
  $webform_request = \Drupal::service('webform.request');
  /** @var \Drupal\webform\WebformLibrariesManagerInterface $libraries_manager */
  $libraries_manager = \Drupal::service('webform.libraries_manager');

  $element = &$variables['element'];

  // Move #description to #help for webform admin routes.
  if (\Drupal::config('webform.settings')->get('ui.description_help')
    && $webform_request->isWebformAdminRoute()
    && $libraries_manager->isIncluded('tippyjs')
    && \Drupal::routeMatch()->getRouteName() !== 'webform.contribute.settings'
    && !isset($element['#help'])
    && !empty($element['#title']) && (empty($element['#title_display']) || !in_array($element['#title_display'], ['attribute', 'invisible']))
    && !empty($element['#description']) && (empty($element['#description_display']) || !in_array($element['#description_display'], ['invisible']))
  ) {
    // Render the description.
    $description = (is_array($element['#description'])) ? \Drupal::service('renderer')->render($element['#description']) : $element['#description'];
    // Replace breaks in admin tooltips with horizontal rules.
    $description = str_replace('<br /><br />', '<hr />', $description);
    $element['#help'] = ['#markup' => $description];

    // We must still render the description as visually hidden because the input
    // has an 'aria-describedby' attribute pointing to the description's id.
    $variables['description_display'] = 'after';
    $variables['description']['content']['description']['#attributes']->addClass('visually-hidden');

    // Remove all links from the #description since it will be .visually-hidden
    // and unreachable via tabbing.
    if (isset($variables['description']['content']['description']['#markup'])) {
      $variables['description']['content']['description']['#markup'] = strip_tags($variables['description']['content']['description']['#markup']);
    }
  }
}

/**
 * Append #help to title or element variable.
 */
function _webform_preprocess_help(array &$variables, $title_parents = ['title']) {
  $element = &$variables['element'];
  $type = $element['#type'] ?? '';

  if (empty($element['#help'])) {
    return;
  }

  $help_display = $element['#help_display'] ?? 'title_after';

  // Determine target variable (aka render element).
  $targets = [
    'title_before' => 'title',
    'title_after' => 'title',
    // Details don't support prefix and suffix.
    // @see details.html.twig
    'element_before' => ($type === 'details') ? 'children' : 'prefix',
    'element_after' => ($type === 'details') ? 'children' : 'suffix',
  ];
  $target = $targets[$help_display];

  // Determine the target element.
  if ($target === 'title') {
    // User title parent to the title (target) element.
    $target_element =& NestedArray::getValue($variables, $title_parents);
    // Empty title should not display help.
    if (empty($target_element)) {
      return;
    }
  }
  else {
    $variables += [$target => []];
    $target_element = &$variables[$target];
  }

  // Default #help_title to element's #title.
  if (empty($element['#help_title']) && !empty($element['#title'])) {
    $element['#help_title'] = $element['#title'];
  }

  $build = [];
  if (!empty($target_element)) {
    $build[$target] = (is_array($target_element)) ? $target_element : ['#markup' => $target_element];
  }
  $build['help'] = [
    '#type' => 'webform_help',
  ] + array_intersect_key($element, array_flip(['#help', '#help_title']));

  // Add help attributes.
  if (isset($element['#help_attributes'])) {
    $build['help']['#attributes'] = $element['#help_attributes'];
  }

  // Get #title_display and move help before title for 'inline' titles.
  if (isset($element['#_title_display'])) {
    // #_title_display is set via WebformElementBase::prepare.
    // @see \Drupal\webform\Plugin\WebformElementBase::prepare.
    $title_display = $element['#_title_display'];
  }
  elseif (isset($element['#title_display'])) {
    $title_display = $element['#title_display'];
  }
  else {
    $title_display = NULL;
  }

  // Place help before the target.
  if (isset($build[$target])) {
    if (($target === 'title' && $title_display === 'inline')
      || $help_display === 'title_before'
      || $help_display === 'element_before') {
      $build[$target]['#weight'] = 0;
      $build['help']['#weight'] = -1;
    }
  }

  // Add help container classes to element wrapper.
  $variables['attributes']['class'][] = Html::getClass('webform-element-help-container--' . preg_replace('/(_after|_before)/', '', $help_display));
  $variables['attributes']['class'][] = Html::getClass('webform-element-help-container--' . $help_display);

  // Replace $variables with new render array containing help.
  $target_element = $build;
}

/**
 * Prepares #more property for form element template.
 *
 * @see template_preprocess_form_element()
 * @see form-element.html.twig
 * @see template_preprocess_datetime_wrapper()
 * @see datetime-wrapper.html.twig
 */
function _webform_preprocess_form_element_description_more(array &$variables) {
  $element = &$variables['element'];
  if (empty($element['#more'])) {
    return;
  }

  // Make sure description is displayed.
  if (!isset($variables['description_display'])) {
    $variables['description_display'] = 'after';
  }

  // Add more element.
  $variables['description']['content']['more'] = [
    '#type' => 'webform_more',
    '#attributes' => (!empty($element['#id'])) ? ['id' => $element['#id'] . '--more'] : [],
  ] + array_intersect_key($element, array_flip(['#more', '#more_title']));
}

/* ************************************************************************** */
// Theme suggestions.
/* ************************************************************************** */

/**
 * Provides alternate named suggestions for a specific theme hook.
 *
 * @param array $variables
 *   An array of variables passed to the theme hook.
 * @param string $hook
 *   The base hook name.
 *
 * @return array
 *   An array of theme suggestions.
 */
function _webform_theme_suggestions(array $variables, $hook) {
  $suggestions = [];

  if ($hook === 'webform' && isset($variables['element']) && isset($variables['element']['#webform_id'])) {
    $suggestions[] = $hook . '__' . $variables['element']['#webform_id'];
  }
  elseif ($hook === 'webform_submission_form' && isset($variables['form']) && isset($variables['form']['#webform_id'])) {
    $suggestions[] = $hook . '__' . $variables['form']['#webform_id'];
  }
  elseif (strpos($hook, 'webform_element_base_') === 0 || strpos($hook, 'webform_container_base_') === 0) {
    $element = $variables['element'];

    if (!empty($element['#type'])) {
      $type = $element['#type'];
      $name = $element['#webform_key'];

      $suggestions[] = $hook . '__' . $type;
      $suggestions[] = $hook . '__' . $type . '__' . $name;
    }
  }
  else {
    $webform = NULL;
    $webform_submission = NULL;
    $sanitized_view_mode = NULL;
    if (isset($variables['elements']) && isset($variables['elements']['#webform_submission'])) {
      /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
      $webform_submission = $variables['elements']['#webform_submission'];
      $webform = $webform_submission->getWebform();
      $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
    }
    elseif (isset($variables['webform_submission'])) {
      /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
      $webform_submission = $variables['webform_submission'];
      $webform = $webform_submission->getWebform();
    }
    elseif (isset($variables['webform'])) {
      /** @var \Drupal\webform\WebformInterface $webform */
      $webform = $variables['webform'];
    }

    if ($webform) {
      $suggestions[] = $hook . '__' . $webform->id();
      if (isset($variables['handler'])) {
        /** @var \Drupal\webform\Plugin\WebformHandlerInterface $handler */
        $handler = $variables['handler'];
        $suggestions[] = $hook . '__' . $webform->id() . '__' . $handler->getPluginId();
        $suggestions[] = $hook . '__' . $webform->id() . '__' . $handler->getPluginId() . '__' . $handler->getHandlerId();
      }
      if ($sanitized_view_mode) {
        $suggestions[] = $hook . '__' . $webform->id() . '__' . $sanitized_view_mode;
        $suggestions[] = $hook . '__' . $sanitized_view_mode;
      }
    }
  }

  return $suggestions;
}

/**
 * Helper function used to generate hook_theme_suggestions_HOOK().
 */
function _webform_devel_hook_theme_suggestions_generate() {
  $theme = webform_theme();
  print '<pre>';
  foreach ($theme as $hook => $info) {
    $suggestion = FALSE;
    if ($hook === 'webform') {
      $suggestion = TRUE;
    }
    elseif (strpos($hook, 'webform_element_base_') === 0 || strpos($hook, 'webform_container_base_') === 0) {
      $suggestion = TRUE;
    }
    elseif (isset($info['variables'])
      && !array_key_exists('element', $info['variables'])
      && (array_key_exists('webform_submission', $info['variables']) || array_key_exists('webform', $info['variables']))) {
      $suggestion = TRUE;
    }

    if ($suggestion) {
      print "/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_$hook(array \$variables) {
  return _webform_theme_suggestions(\$variables, '$hook');
}

";
    }
  }
  print '</pre>';
  exit;
}

/* ************************************************************************** */
// Webform theme suggestions.
// Generate using _webform_devel_hook_theme_suggestions_generate();
/* ************************************************************************** */

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform(array $variables) {
  return _webform_theme_suggestions($variables, 'webform');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_confirmation(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_confirmation');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_preview(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_preview');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_submission(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_submission');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_submission_form(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_submission_form');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_submission_navigation(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_submission_navigation');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_submission_information(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_submission_information');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_submission_data(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_submission_data');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_element_base_html(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_element_base_html');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_element_base_text(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_element_base_text');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_container_base_html(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_container_base_html');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_container_base_text(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_container_base_text');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_email_html(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_email_html');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_email_message_html(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_email_message_html');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_email_message_text(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_email_message_text');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_progress(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_progress');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_progress_bar(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_progress_bar');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_webform_progress_tracker(array $variables) {
  return _webform_theme_suggestions($variables, 'webform_progress_tracker');
}

/* ************************************************************************** */
// Custom theme suggestions.
/* ************************************************************************** */

/**
 * Add webform options display suggestions to radios and checkboxes.
 *
 * @param array $variables
 *   Template variables.
 * @param string $type
 *   Type of options element. (checkboxes or radios)
 *
 * @return array
 *   If element include #options_display a template suggestion for
 *   TYPE-OPTIONS-DISPLAY
 */
function _webform_theme_suggestions_options(array $variables, $type) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return [];
  }

  $element = $variables['element'];
  $suggestions = [];
  if (!empty($element['#options_display']) && strpos($element['#options_display'], 'buttons') === 0) {
    $suggestions[] = $type . '__webform_' . $element['#options_display'];
  }
  return $suggestions;
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_radios(array $variables) {
  return _webform_theme_suggestions_options($variables, 'radios');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_checkboxes(array $variables) {
  return _webform_theme_suggestions_options($variables, 'checkboxes');
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function webform_theme_suggestions_form_element(array $variables) {
  if (!WebformElementHelper::isWebformElement($variables['element'])) {
    return [];
  }
  $element = $variables['element'];

  $suggestions = [];

  // Add webform type suggestion.
  if (isset($element['#type'])) {
    $suggestions[] = 'form_element__webform_' . $element['#type'];
  }

  // Add radio/checkbox displayed as a button suggestion.
  // @sse webform_process_options()
  if (!empty($element['#option_display']) && $element['#option_display'] === 'button') {
    $suggestions[] = 'form_element__webform_' . $element['#type'] . '_' . $element['#option_display'];
  }

  return $suggestions;
}
