Repository: department-of-veterans-affairs/va.gov-cms
Pull request #21164: VAGOV-TEAM-106479: Response kind
Author: login: ryguyk
Created: 2025-04-14T15:58:33Z | Merged: 2025-04-14T21:54:34Z

## Description

Closes https://github.com/department-of-veterans-affairs/va.gov-team/issues/106479

## Testing done
- Manual testing done
- Some unit tests added

## Screenshots
![image](https://github.com/user-attachments/assets/20ef80fd-25d2-4947-8ea4-b244919199d1)

## QA steps
As a Form Builder user:
1. On the Form Builder home page, select an existing form or create a new one.
1. On the form-layout page, click "Add step".
1. Add the step.
1. On the step-layout page click "Add question".
1. On the following page, click "Customize".
  - [x] Validate you are taken to the new response-kind page.
1.  On the new response-kind page:
  - [x] Validate that the breadcrumbs (for now) include: "Custom or predefined > Response kind".
  - [x] Validate that there are three radio options.
  - [x] Validate the primary button reads "Save and continue".
1. Select on one of the radio buttons and then click "Save and continue".
  - [x] Validate that you are (for now) taken to the step-layout page.
  - [x] Validate that the url has a query parameter indicating the `response_kind` value you selected.
  - (This is a way to confirm that the form is working properly. We will replace this behavior with proper routing once the subsequent pages exist).

### Definition of Done

- [ ] Documentation has been updated, if applicable.
- [ ] Tests have been added if necessary.
- [ ] Automated tests have passed.
- [ ] Code Quality Tests have passed.
- [ ] Acceptance Criteria in related issue are met.
- [ ] Manual Code Review Approved.
- [ ] If there are field changes, front end output has been thoroughly checked.

### Select Team for PR review

- [ ] `CMS Team`
- [ ] `Public websites`
- [ ] `Facilities`
- [ ] `User support`
- [ ] `Accelerated Publishing`
- [x] `Form Engine`


### Is this PR blocked by another PR?

- [ ] `DO NOT MERGE`

### Does this PR need review from a Product Owner

- [ ] `Needs PO review`

### CMS user-facing announcement

Is an announcement needed to let editors know of this change?
- [ ] Yes, and it's written in issue ____ and queued for publication.
  - [ ] Merge and ping the UX writer so they are ready to publish after deployment
- [ ] Yes, but it hasn't yet been written
  - [ ] Don't merge yet -- ping the UX writer to write and queue content
- [x] No announcement is needed for this code change.
  - [ ] Merge & carry on unburdened by announcements



Diff:
diff --git a/docroot/modules/custom/va_gov_form_builder/css/expanded-radio--help-text-optional-image.css b/docroot/modules/custom/va_gov_form_builder/css/expanded-radio--help-text-optional-image.css
new file mode 100644
index 00000000000..612cded7fe4
--- /dev/null
+++ b/docroot/modules/custom/va_gov_form_builder/css/expanded-radio--help-text-optional-image.css
@@ -0,0 +1,7 @@
+.form-builder-expanded-radio-help-text-optional-image__image-with-caption {
+  margin: var(--units-4) 0;
+}
+
+.form-builder-expanded-radio-help-text-optional-image__image {
+  border: 4px solid var(--vads-color-base-lightest);
+}
diff --git a/docroot/modules/custom/va_gov_form_builder/css/expanded-radio.css b/docroot/modules/custom/va_gov_form_builder/css/expanded-radio.css
new file mode 100644
index 00000000000..ce81e17628b
--- /dev/null
+++ b/docroot/modules/custom/va_gov_form_builder/css/expanded-radio.css
@@ -0,0 +1,12 @@
+.form-builder-expanded-radio {
+  margin-top: var(--units-5);
+}
+
+.form-builder-expanded-radio label {
+  font-size: var(---font-size-md);
+}
+
+.form-builder-expanded-radio__expanded-content {
+  font-size: var(--font-size-sm);
+  margin-top: var(--units-2);
+}
diff --git a/docroot/modules/custom/va_gov_form_builder/css/form-builder.css b/docroot/modules/custom/va_gov_form_builder/css/form-builder.css
index 9ed218b5b36..76cb78c5044 100644
--- a/docroot/modules/custom/va_gov_form_builder/css/form-builder.css
+++ b/docroot/modules/custom/va_gov_form_builder/css/form-builder.css
@@ -98,7 +98,8 @@
 /**
  * required fields
  */
-.form-builder-page-container .form-item__label.form-required::after {
+.form-builder-page-container .form-item__label.form-required::after,
+.form-builder-page-container .fieldset__label.form-required::after {
   color: var(--vads-color-secondary-dark);
   content: "(*Required)";
 }
diff --git a/docroot/modules/custom/va_gov_form_builder/css/response-kind.css b/docroot/modules/custom/va_gov_form_builder/css/response-kind.css
new file mode 100644
index 00000000000..1161d4b1993
--- /dev/null
+++ b/docroot/modules/custom/va_gov_form_builder/css/response-kind.css
@@ -0,0 +1,3 @@
+#form-builder-content-response-kind {
+  width: 500px;
+}
diff --git a/docroot/modules/custom/va_gov_form_builder/images/response-kind-choice.png b/docroot/modules/custom/va_gov_form_builder/images/response-kind-choice.png
new file mode 100644
index 00000000000..4ef39b3e8fb
Binary files /dev/null and b/docroot/modules/custom/va_gov_form_builder/images/response-kind-choice.png differ
diff --git a/docroot/modules/custom/va_gov_form_builder/images/response-kind-date.png b/docroot/modules/custom/va_gov_form_builder/images/response-kind-date.png
new file mode 100644
index 00000000000..a9b2b071165
Binary files /dev/null and b/docroot/modules/custom/va_gov_form_builder/images/response-kind-date.png differ
diff --git a/docroot/modules/custom/va_gov_form_builder/images/response-kind-text.png b/docroot/modules/custom/va_gov_form_builder/images/response-kind-text.png
new file mode 100644
index 00000000000..f23160d6d80
Binary files /dev/null and b/docroot/modules/custom/va_gov_form_builder/images/response-kind-text.png differ
diff --git a/docroot/modules/custom/va_gov_form_builder/src/Controller/VaGovFormBuilderController.php b/docroot/modules/custom/va_gov_form_builder/src/Controller/VaGovFormBuilderController.php
index 5249032e084..9f1d15bf65c 100644
--- a/docroot/modules/custom/va_gov_form_builder/src/Controller/VaGovFormBuilderController.php
+++ b/docroot/modules/custom/va_gov_form_builder/src/Controller/VaGovFormBuilderController.php
@@ -208,6 +208,7 @@ protected function getPageUrl($page) {
       'step.layout',
       'step.step_label.edit',
       'step.question.custom_or_predefined',
+      'step.question.custom.kind',
     ];
     if (in_array($page, $stepPages)) {
       if (!$this->digitalForm) {
@@ -304,6 +305,19 @@ protected function generateBreadcrumbs($parent, $label, $url = NULL) {
       );
     }
 
+    elseif ($parent === 'step.question.custom_or_predefined') {
+      if (!$this->digitalForm || !$this->stepParagraph) {
+        return [];
+      }
+
+      $customOrPredefinedUrl = $this->getPageUrl('step.question.custom_or_predefined');
+      $breadcrumbTrail = $this->generateBreadcrumbs(
+        'step.layout',
+        'Custom or predefined',
+        $customOrPredefinedUrl
+      );
+    }
+
     $breadcrumbTrail[] = [
       'label' => $label,
       'url' => $url ? $url : '#content',
@@ -832,6 +846,7 @@ public function customOrPredefinedQuestion($nid, $stepParagraphId) {
 
     if ($stepType === 'digital_form_custom_step') {
       $pageContent['#theme'] = self::PAGE_CONTENT_THEME_PREFIX . 'custom_or_predefined_question__single_question';
+      $pageContent['#buttons']['primary']['url'] = $this->getPageUrl('step.question.custom.kind');
     }
     else {
       $pageContent['#theme'] = self::PAGE_CONTENT_THEME_PREFIX . 'custom_or_predefined_question__repeating_set';
@@ -844,6 +859,31 @@ public function customOrPredefinedQuestion($nid, $stepParagraphId) {
     return $this->getPage($pageContent, $subtitle, $breadcrumbs, $libraries);
   }
 
+  /**
+   * Response-kind page for custom single-question questions.
+   *
+   * @param string $nid
+   *   The node id of the Digital Form.
+   * @param string $stepParagraphId
+   *   The entity id of the step paragraph.
+   */
+  public function customSingleQuestionResponseKind($nid, $stepParagraphId) {
+    $this->loadDigitalForm($nid);
+    $this->loadStepParagraph($stepParagraphId);
+
+    $formName = 'ResponseKind';
+    $breadcrumbs = $this->generateBreadcrumbs('step.question.custom_or_predefined', 'Response kind');
+    $subtitle = $this->digitalForm->getTitle();
+    $libraries = [
+      'single_column_with_buttons',
+      'response_kind',
+      'expanded_radio',
+      'expanded_radio__help_text_optional_image',
+    ];
+
+    return $this->getFormPage($formName, $subtitle, $breadcrumbs, $libraries);
+  }
+
   /**
    * Review-and-sign page.
    *
diff --git a/docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadio.php b/docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadio.php
new file mode 100644
index 00000000000..23870e82dd1
--- /dev/null
+++ b/docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadio.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\va_gov_form_builder\Element;
+
+use Drupal\Core\Render\Element\Radio;
+
+/**
+ * Defines a custom expanded-radio input for a single radio.
+ *
+ * This is an empty class. The definition needs to be here to define
+ * a form element of type `va_gov_form_builder__expanded_radio`,
+ * but the class itself does not need to do anything different than
+ * the parent class. When this form element is called for an individual
+ * radio item of type `va_gov_form_builder__expanded_radios`, it will
+ * be rendered by the appropriate template due to theme suggestions
+ * defined in `hook_theme` of `va_gov_form_builder.module`.
+ *
+ * The template used to render this element is
+ * `va_gov_form_builder/templates/expanded-radio.html.twig`.
+ *
+ * @FormElement("va_gov_form_builder__expanded_radio")
+ */
+class ExpandedRadio extends Radio {
+}
diff --git a/docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadios.php b/docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadios.php
new file mode 100644
index 00000000000..b755c40adc1
--- /dev/null
+++ b/docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadios.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\va_gov_form_builder\Element;
+
+use Drupal\Core\Render\Element\Radios;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Defines a custom expanded-radio input.
+ *
+ * @FormElement("va_gov_form_builder__expanded_radios")
+ */
+class ExpandedRadios extends Radios {
+
+  /**
+   * Configures each individual radio item.
+   */
+  public static function processRadios(&$element, FormStateInterface $form_state, &$complete_form) {
+    $element = parent::processRadios($element, $form_state, $complete_form);
+    foreach ($element['#options'] as $key => $choice) {
+      $element[$key]['#type'] = 'va_gov_form_builder__expanded_radio';
+      $element[$key]['#expanded_content'] = $element['#options_expanded_content'][$key] ?? [];
+    }
+
+    return $element;
+  }
+
+}
diff --git a/docroot/modules/custom/va_gov_form_builder/src/Form/ResponseKind.php b/docroot/modules/custom/va_gov_form_builder/src/Form/ResponseKind.php
new file mode 100644
index 00000000000..874c746599d
--- /dev/null
+++ b/docroot/modules/custom/va_gov_form_builder/src/Form/ResponseKind.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\va_gov_form_builder\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\va_gov_form_builder\Form\Base\FormBuilderBase;
+
+/**
+ * Form step for choosing a question's response kind.
+ */
+class ResponseKind extends FormBuilderBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'form_builder__response_kind';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['#theme'] = 'form__va_gov_form_builder__response_kind';
+
+    $imageDir = '/modules/custom/va_gov_form_builder/images/';
+    $form['response_kind'] = [
+      '#type' => 'va_gov_form_builder__expanded_radios',
+      '#title' => $this->t('What kind of response will the submitter provide?'),
+      '#options' => [
+        'choice' => $this->t('Choice'),
+        'date' => $this->t('Date'),
+        'text' => $this->t('Text and/or numbers'),
+      ],
+      '#options_expanded_content' => [
+        'choice' => [
+          '#theme' => 'page_element__va_gov_form_builder__expanded_radio__help_text_optional_image',
+          '#help_text' => $this->t('This allows one or multiple items to be selected from a provided list.'),
+          '#image' => [
+            'alt_text' => 'Example of a list of choices',
+            'caption' => 'Example of a list of choices',
+            'url' => $imageDir . 'response-kind-choice.png',
+          ],
+        ],
+        'date' => [
+          '#theme' => 'page_element__va_gov_form_builder__expanded_radio__help_text_optional_image',
+          '#help_text' => $this->t('This allows a specific or approximate date or date range to be input.'),
+          '#image' => [
+            'alt_text' => 'Example of a date range',
+            'caption' => 'Example of a date range',
+            'url' => $imageDir . 'response-kind-date.png',
+          ],
+        ],
+        'text' => [
+          '#theme' => 'page_element__va_gov_form_builder__expanded_radio__help_text_optional_image',
+          '#help_text' => $this->t('This allows the submitter to fill in the blank for the question you are asking.'),
+          '#image' => [
+            'alt_text' => 'Example of input fields',
+            'caption' => 'Example of input fields',
+            'url' => $imageDir . 'response-kind-text.png',
+          ],
+        ],
+      ],
+      '#required' => TRUE,
+    ];
+
+    $form['actions']['save_and_continue'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Save and continue'),
+      '#attributes' => [
+        'class' => [
+          'button',
+          'button--primary',
+          'form-submit',
+        ],
+      ],
+      '#weight' => '10',
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $routeParameters = \Drupal::routeMatch()->getParameters()->all();
+    $nid = $routeParameters['nid'];
+    $stepParagraphId = $routeParameters['stepParagraphId'];
+
+    $responseKind = $form_state->getValue('response_kind');
+
+    switch ($responseKind) {
+      // Eventually, we'll have to handle these individually.
+      // For now, we just redirect to an arbitrary page with
+      // the response kind as a query parameter.
+      case 'choice':
+      case 'date':
+      case 'text':
+        $form_state->setRedirect('va_gov_form_builder.step.layout', [
+          'nid' => $nid,
+          'stepParagraphId' => $stepParagraphId,
+        ], [
+          'query' => [
+            'response_kind' => $responseKind,
+          ],
+        ]);
+        break;
+    }
+  }
+
+}
diff --git a/docroot/modules/custom/va_gov_form_builder/templates/form-elements/expanded-radio.html.twig b/docroot/modules/custom/va_gov_form_builder/templates/form-elements/expanded-radio.html.twig
new file mode 100644
index 00000000000..f6b044a67cb
--- /dev/null
+++ b/docroot/modules/custom/va_gov_form_builder/templates/form-elements/expanded-radio.html.twig
@@ -0,0 +1,51 @@
+{#
+/**
+ * @file
+ * Template for an expanded radio option.
+ *
+ * @see docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadio.php
+ * @see docroot/themes/custom/vagovclaro/templates/form/form-element.html.twig
+ */
+#}
+{%
+  set classes = [
+    'js-form-item',
+    'form-item',
+    'js-form-type-' ~ type|clean_class,
+    'form-type--' ~ type|clean_class,
+    'form-type--boolean',
+    'js-form-item-' ~ name|clean_class,
+    'form-item--' ~ name|clean_class,
+    disabled == 'disabled' ? 'form-item--disabled',
+    errors ? 'form-item--error',
+  ]
+%}
+<div{{ attributes.addClass(classes) }}>
+  {% if prefix is not empty %}
+    <span class="form-item__prefix{{disabled == 'disabled' ? ' is-disabled'}}">{{ prefix }}</span>
+  {% endif %}
+
+  <div class="form-builder-expanded-radio">
+    {# This renders the actual radio input #}
+    {{ children }}
+
+    <div class="form-builder-expanded-radio__content">
+      {{ label }}
+
+      {# This is where the custom expanded content gets rendered #}
+      <div class="form-builder-expanded-radio__expanded-content">
+        {{ element['#expanded_content'] }}
+      </div>
+    </div>
+  </div>
+
+  {% if suffix is not empty %}
+    <span class="form-item__suffix{{disabled == 'disabled' ? ' is-disabled'}}">{{ suffix }}</span>
+  {% endif %}
+
+  {% if errors %}
+    <div class="form-item__error-message">
+      {{ errors }}
+    </div>
+  {% endif %}
+</div>
diff --git a/docroot/modules/custom/va_gov_form_builder/templates/form/response-kind.html.twig b/docroot/modules/custom/va_gov_form_builder/templates/form/response-kind.html.twig
new file mode 100644
index 00000000000..18893e246e9
--- /dev/null
+++ b/docroot/modules/custom/va_gov_form_builder/templates/form/response-kind.html.twig
@@ -0,0 +1,23 @@
+{#
+/**
+ * @file
+ * Template for Form Builder step page: Step Label
+ */
+#}
+{% extends '@va_gov_form_builder/form/base/single-column-with-buttons.html.twig' %}
+
+{% set page_id = 'response-kind' %}
+{% set page_heading = 'Select response kind' %}
+
+{% block main_content %}
+  <p>We'll add the question/statement over the course of a few screens.</p>
+  {{ form.response_kind }}
+{% endblock %}
+
+{% block after_buttons %}
+  {# render rest of form so it functions properly #}
+  {{ form|without(
+    'response_kind',
+    'actions',
+  )}}
+{% endblock %}
diff --git a/docroot/modules/custom/va_gov_form_builder/templates/page-elements/expanded-radio--help-text-optional-image.html.twig b/docroot/modules/custom/va_gov_form_builder/templates/page-elements/expanded-radio--help-text-optional-image.html.twig
new file mode 100644
index 00000000000..c976edc1463
--- /dev/null
+++ b/docroot/modules/custom/va_gov_form_builder/templates/page-elements/expanded-radio--help-text-optional-image.html.twig
@@ -0,0 +1,20 @@
+<div class="form-builder-expanded-radio-help-text-optional-image">
+  <div class="form-builder-expanded-radio-help-text-optional-image__help_text">
+    {{ help_text }}
+  </div>
+
+  {% if image is not empty %}
+    <figure class="form-builder-expanded-radio-help-text-optional-image__image-with-caption">
+      <img
+        src="{{ image.url }}"
+        alt="{{ image.alt }}"
+        class="form-builder-expanded-radio-help-text-optional-image__image"
+      />
+      <figcaption
+        class="form-builder-expanded-radio-help-text-optional-image__caption"
+      >
+        {{ image.caption}}
+      </figcaption>
+    </figure>
+  {% endif %}
+</div>
diff --git a/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.libraries.yml b/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.libraries.yml
index 57d09002cae..cb58a4dec43 100644
--- a/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.libraries.yml
+++ b/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.libraries.yml
@@ -50,6 +50,21 @@ custom_or_predefined_question:
   css:
     theme:
       css/custom-or-predefined-question.css: {}
+response_kind:
+  version: 1.x
+  css:
+    theme:
+      css/response-kind.css: {}
+expanded_radio:
+  version: 1.x
+  css:
+    theme:
+      css/expanded-radio.css: {}
+expanded_radio__help_text_optional_image:
+  version: 1.x
+  css:
+    theme:
+      css/expanded-radio--help-text-optional-image.css: {}
 paragraph_sort_and_delete:
   version: 1.x
   js:
diff --git a/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.module b/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.module
index 247c83fe58f..6aaf3de4d4d 100644
--- a/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.module
+++ b/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.module
@@ -177,7 +177,7 @@ function va_gov_form_builder_theme($_existing, $_type, $_theme, $path) {
   ];
 
   // Add form themes.
-  $forms = ['form_info', 'step_label', 'step_style'];
+  $forms = ['form_info', 'step_label', 'step_style', 'response_kind'];
   foreach ($forms as $form) {
     $kebab_case_form = str_replace('_', '-', $form);
     $theme['form__va_gov_form_builder__' . $form] = [
@@ -187,11 +187,32 @@ function va_gov_form_builder_theme($_existing, $_type, $_theme, $path) {
     ];
   }
 
+  // Add form-element themes.
+  $form_element_theme_prefix = 'form_element__va_gov_form_builder__';
+  // 1. Expanded radio.
+  $theme[$form_element_theme_prefix . 'expanded_radio'] = [
+    'base hook' => 'radio',
+    'path' => $path . '/templates/form-elements',
+    'template' => 'expanded-radio',
+  ];
+
+  // Add page-element themes.
+  $page_element_theme_prefix = 'page_element__va_gov_form_builder__';
+  // 1. Expanded-radio -- Help text and optional image.
+  $theme[$page_element_theme_prefix . 'expanded_radio__help_text_optional_image'] = [
+    'path' => $path . '/templates/page-elements',
+    'template' => 'expanded-radio--help-text-optional-image',
+    'variables' => [
+      'help_text' => '',
+      'image' => [],
+    ],
+  ];
+
   return $theme;
 }
 
 /**
- * Implements hook_theme_suggestions_HOOK().
+ * Implements hook_theme_suggestions_HOOK() for page.
  */
 function va_gov_form_builder_theme_suggestions_page(array &$variables) {
   $route_name = \Drupal::routeMatch()->getRouteName();
diff --git a/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.routing.yml b/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.routing.yml
index 0387d4533be..ac901a3f81b 100644
--- a/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.routing.yml
+++ b/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.routing.yml
@@ -79,6 +79,13 @@ va_gov_form_builder.step.question.custom_or_predefined:
   requirements:
     nid: \d*
     stepParagraphId: \d*
+va_gov_form_builder.step.question.custom.kind:
+  path: "/form-builder/{nid}/step/{stepParagraphId}/question/add/custom/kind"
+  defaults:
+    _controller: '\Drupal\va_gov_form_builder\Controller\VaGovFormBuilderController::customSingleQuestionResponseKind'
+  requirements:
+    nid: \d*
+    stepParagraphId: \d*
 va_gov_form_builder.review_and_sign:
   path: "/form-builder/{nid}/review-and-sign"
   defaults:
diff --git a/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.services.yml b/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.services.yml
index 507dbd20a91..1fd6641de05 100644
--- a/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.services.yml
+++ b/docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.services.yml
@@ -2,7 +2,7 @@ services:
   va_gov_form_builder.route_subscriber:
     class: Drupal\va_gov_form_builder\Routing\RouteSubscriber
     tags:
-      - { name: "event_subscriber" }
+      - { name: event_subscriber }
   va_gov_form_builder.digital_forms_service:
     class: Drupal\va_gov_form_builder\Service\DigitalFormsService
     arguments: ["@entity_type.manager"]
diff --git a/tests/phpunit/va_gov_form_builder/unit/LibrariesTest.php b/tests/phpunit/va_gov_form_builder/unit/LibrariesTest.php
index 8a4ea92812d..5b0d1c02542 100644
--- a/tests/phpunit/va_gov_form_builder/unit/LibrariesTest.php
+++ b/tests/phpunit/va_gov_form_builder/unit/LibrariesTest.php
@@ -110,12 +110,27 @@ public function testLibraryCss() {
     $this->assertArrayHasKey($stepStyleLibrary, $this->libraries);
     $stepStyleCssArray = array_keys($this->libraries[$stepStyleLibrary]['css']['theme']);
     $this->assertContains($cssPrefix . 'step-style.css', $stepStyleCssArray, 'Step-style css is present.');
-    // 9. Custom-or-predefined-question.
+    // 9. Custom-or-predefined question.
     $customOrPredefinedQuestionLibrary = 'custom_or_predefined_question';
     $this->assertArrayHasKey($customOrPredefinedQuestionLibrary, $this->libraries);
     $customOrPredefinedQuestionCssArray = array_keys($this->libraries[$customOrPredefinedQuestionLibrary]['css']['theme']);
     $this->assertContains($cssPrefix . 'custom-or-predefined-question.css', $customOrPredefinedQuestionCssArray, 'Custom-or-predefined-question css is present.');
-    // 10. Paragraph sort and delete.
+    // 10. Response kind.
+    $responseKindLibrary = 'response_kind';
+    $this->assertArrayHasKey($responseKindLibrary, $this->libraries);
+    $responseKindCssArray = array_keys($this->libraries[$responseKindLibrary]['css']['theme']);
+    $this->assertContains($cssPrefix . 'response-kind.css', $responseKindCssArray, 'Response-kind css is present.');
+    // 11. Expanded radio.
+    $expandedRadioLibrary = 'expanded_radio';
+    $this->assertArrayHasKey($expandedRadioLibrary, $this->libraries);
+    $expandedRadioCssArray = array_keys($this->libraries[$expandedRadioLibrary]['css']['theme']);
+    $this->assertContains($cssPrefix . 'expanded-radio.css', $expandedRadioCssArray, 'Expanded-radio css is present.');
+    // 12. Expanded radio -- Help text with optional image.
+    $expandedRadioHelpTextLibrary = 'expanded_radio__help_text_optional_image';
+    $this->assertArrayHasKey($expandedRadioHelpTextLibrary, $this->libraries);
+    $expandedRadioHelpTextCssArray = array_keys($this->libraries[$expandedRadioHelpTextLibrary]['css']['theme']);
+    $this->assertContains($cssPrefix . 'expanded-radio--help-text-optional-image.css', $expandedRadioHelpTextCssArray, 'Expanded-radio -- Help text with optional image css is present.');
+    // 13. Paragraph sort and delete.
     $paragraphSortAndDeleteLibrary = 'paragraph_sort_and_delete';
     $this->assertArrayHasKey($paragraphSortAndDeleteLibrary, $this->libraries);
     $paragraphSortAndDeleteJsArray = array_keys($this->libraries[$paragraphSortAndDeleteLibrary]['js']);
diff --git a/tests/phpunit/va_gov_form_builder/unit/ModuleTest.php b/tests/phpunit/va_gov_form_builder/unit/ModuleTest.php
index ac9ae69800e..555308be4a3 100644
--- a/tests/phpunit/va_gov_form_builder/unit/ModuleTest.php
+++ b/tests/phpunit/va_gov_form_builder/unit/ModuleTest.php
@@ -40,6 +40,16 @@ class ModuleTest extends VaGovUnitTestBase {
    */
   const FORM_TEMPLATE_PATH = self::TEMPLATE_PATH . '/form';
 
+  /**
+   * The path to the form-element template directory.
+   */
+  const FORM_ELEMENT_TEMPLATE_PATH = self::TEMPLATE_PATH . '/form-elements';
+
+  /**
+   * The path to the page-element template directory.
+   */
+  const PAGE_ELEMENT_TEMPLATE_PATH = self::TEMPLATE_PATH . '/page-elements';
+
   /**
    * The prefix for page-content themes.
    */
@@ -50,6 +60,16 @@ class ModuleTest extends VaGovUnitTestBase {
    */
   const FORM_THEME_PREFIX = 'form__va_gov_form_builder__';
 
+  /**
+   * The prefix for form-element themes.
+   */
+  const FORM_ELEMENT_THEME_PREFIX = 'form_element__va_gov_form_builder__';
+
+  /**
+   * The prefix for page-element themes.
+   */
+  const PAGE_ELEMENT_THEME_PREFIX = 'page_element__va_gov_form_builder__';
+
   /**
    * A mock service container.
    *
@@ -263,7 +283,7 @@ public function testVaGovFormBuilderHookTheme() {
     );
 
     // Form themes.
-    $form_themes = ['form_info', 'step_label', 'step_style'];
+    $form_themes = ['form_info', 'step_label', 'step_style', 'response_kind'];
     foreach ($form_themes as $form_theme) {
       $this->assertArrayHasKey(self::FORM_THEME_PREFIX . $form_theme, $result);
 
@@ -277,6 +297,29 @@ public function testVaGovFormBuilderHookTheme() {
       $kebab_case_form_theme = str_replace('_', '-', $form_theme);
       $this->assertEquals($kebab_case_form_theme, $result[self::FORM_THEME_PREFIX . $form_theme]['template']);
     }
+
+    // Form-element themes.
+    // 1. Expanded radio.
+    $expandedRadioTheme = self::FORM_ELEMENT_THEME_PREFIX . 'expanded_radio';
+    $this->assertArrayHasKey($expandedRadioTheme, $result);
+    $this->assertArrayHasKey('base hook', $result[$expandedRadioTheme]);
+    $this->assertEquals('radio', $result[$expandedRadioTheme]['base hook']);
+    $this->assertArrayHasKey('path', $result[$expandedRadioTheme]);
+    $this->assertEquals(self::FORM_ELEMENT_TEMPLATE_PATH, $result[$expandedRadioTheme]['path']);
+    $this->assertArrayHasKey('template', $result[$expandedRadioTheme]);
+    $this->assertEquals('expanded-radio', $result[$expandedRadioTheme]['template']);
+
+    // Page-element themes.
+    // 1. Expanded radio -- Help text with optional image.
+    $expandedRadioHelpTextTheme = self::PAGE_ELEMENT_THEME_PREFIX . 'expanded_radio__help_text_optional_image';
+    $this->assertArrayHasKey($expandedRadioHelpTextTheme, $result);
+    $this->assertArrayHasKey('path', $result[$expandedRadioHelpTextTheme]);
+    $this->assertEquals(self::PAGE_ELEMENT_TEMPLATE_PATH, $result[$expandedRadioHelpTextTheme]['path']);
+    $this->assertArrayHasKey('template', $result[$expandedRadioHelpTextTheme]);
+    $this->assertEquals('expanded-radio--help-text-optional-image', $result[$expandedRadioHelpTextTheme]['template']);
+    $this->assertArrayHasKey('variables', $result[$expandedRadioHelpTextTheme]);
+    $this->assertArrayHasKey('help_text', $result[$expandedRadioHelpTextTheme]['variables']);
+    $this->assertArrayHasKey('image', $result[$expandedRadioHelpTextTheme]['variables']);
   }
 
   /**


Reviews:
user: login: ryguyk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:01:47Z
user: login: ryguyk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:03:49Z
user: login: ryguyk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:08:18Z
user: login: ryguyk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:10:51Z
user: login: ryguyk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:11:52Z
user: login: ryguyk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:12:08Z
user: login: ryguyk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:18:00Z
user: login: derekhouck
body: I had one question about a change in the services file. Everything else looks good.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:42:44Z
user: login: ryguyk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:46:28Z

Review comments:
diff_hunk: @@ -304,6 +304,19 @@ protected function generateBreadcrumbs($parent, $label, $url = NULL) {
       );
     }
 
+    elseif ($parent === 'step.question.custom_or_predefined') {
+      if (!$this->digitalForm || !$this->stepParagraph) {
+        return [];
+      }
+
+      $customOrPredefinedUrl = $this->getPageUrl('step.question.custom_or_predefined');
+      $breadcrumbTrail = $this->generateBreadcrumbs(
+        'step.layout',
+        'Custom or predefined',
+        $customOrPredefinedUrl
+      );
+    }
+
path: docroot/modules/custom/va_gov_form_builder/src/Controller/VaGovFormBuilderController.php
user: login: ryguyk
body: If we decide on hierarchy-based breadcrumbs (e.g. `Custom > kind`) rather than action-based breadcrumbs like we have here (e.g. `Custom or predefined > Kind`), this means of generating breadcrumbs will have to be adjusted.
created_at: 2025-04-14T19:01:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -844,6 +857,31 @@ public function customOrPredefinedQuestion($nid, $stepParagraphId) {
     return $this->getPage($pageContent, $subtitle, $breadcrumbs, $libraries);
   }
 
+  /**
+   * Response-kind page for custom single-question questions.
+   *
+   * @param string $nid
+   *   The node id of the Digital Form.
+   * @param string $stepParagraphId
+   *   The entity id of the step paragraph.
+   */
+  public function customSingleQuestionResponseKind($nid, $stepParagraphId) {
+    $this->loadDigitalForm($nid);
+    $this->loadStepParagraph($stepParagraphId);
+
+    $formName = 'ResponseKind';
+    $breadcrumbs = $this->generateBreadcrumbs('step.question.custom_or_predefined', 'Response kind');
+    $subtitle = $this->digitalForm->getTitle();
+    $libraries = [
+      'single_column_with_buttons',
+      'response_kind',
+      'expanded_radio',
+      'expanded_radio__help_text_optional_image',
+    ];
path: docroot/modules/custom/va_gov_form_builder/src/Controller/VaGovFormBuilderController.php
user: login: ryguyk
body: I'd like to consider getting the templates to own the library addition. In this way, if a template is used, the css would come along automatically. This approach where the controller owned this made sense initially (since it is a controller, after all), but it's starting to feel like it would be cleaner for the templates to own this and the controller to not have to worry about the stylesheets each template needs.
created_at: 2025-04-14T19:03:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\va_gov_form_builder\Element;
+
+use Drupal\Core\Render\Element\Radio;
+
+/**
+ * Defines a custom expanded-radio input for a single radio.
+ *
+ * This is an empty class. The definition needs to be here to define
+ * a form element of type `va_gov_form_builder__expanded_radio`,
+ * but the class itself does not need to do anything different than
+ * the parent class. When this form element is called for an individual
+ * radio item of type `va_gov_form_builder__expanded_radios`, it will
+ * be rendered by the appropriate template due to theme suggestions
+ * defined in `hook_theme` of `va_gov_form_builder.module`.
+ *
+ * The template used to render this element is
+ * `va_gov_form_builder/templates/expanded-radio.html.twig`.
+ *
+ * @FormElement("va_gov_form_builder__expanded_radio")
+ */
+class ExpandedRadio extends Radio {
+}
path: docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadio.php
user: login: ryguyk
body: This was a bit of a pain. It's not remotely satisfying to me to have an empty class here, but things were proving incredibly difficult wrestling with Drupal's theme suggestions. I left a comment in the code, as is seen here, to try to explain things. The situation ultimately boils down to the way in which we define themes, how much we are trying to build on the existing radio element (in this case, I wanted to use as much of it as possible, so as to not rewrite all the nuances of rendering and handling radios), how Drupal makes some automatic theme suggestions based on names, and how things have been different trying to name things in such a way to keep Form Builder completely walled off from the rest of the Drupal installation (i.e. defining all of this in our module, entirely cut off from the theme itself `vagovclaro`). 

This was the thing that took the most time with this PR, and while I'm not thrilled with the clarity of the code or the level to which there's some magic going on, the good news is that the outcome of this is having a reusable custom radio element that we can use on multiple pages.
created_at: 2025-04-14T19:08:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\va_gov_form_builder\Element;
+
+use Drupal\Core\Render\Element\Radios;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Defines a custom expanded-radio input.
+ *
+ * @FormElement("va_gov_form_builder__expanded_radios")
+ */
+class ExpandedRadios extends Radios {
+
+  /**
+   * Configures each individual radio item.
+   */
+  public static function processRadios(&$element, FormStateInterface $form_state, &$complete_form) {
+    $element = parent::processRadios($element, $form_state, $complete_form);
+    foreach ($element['#options'] as $key => $choice) {
+      $element[$key]['#type'] = 'va_gov_form_builder__expanded_radio';
path: docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadios.php
user: login: ryguyk
body: This is why we need the (empty) `ExpandedRadio` class to exist. We are going through here and marking each option on the `ExpandedRadios` (`va_gov_form_builder__expanded_radios`) element as being of the `ExpandedRadio` (`va_gov_form_builder__expanded_radio`) type. Even though that class doesn't do anything different from its parent due to the theme suggestions Drupal calculates that allows it to use a different theme from its parent, we still need the class to exist so that this definition can work. If the class didn't exist, this call on line 21 would raise an error.
created_at: 2025-04-14T19:10:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\va_gov_form_builder\Element;
+
+use Drupal\Core\Render\Element\Radios;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Defines a custom expanded-radio input.
+ *
+ * @FormElement("va_gov_form_builder__expanded_radios")
+ */
+class ExpandedRadios extends Radios {
+
+  /**
+   * Configures each individual radio item.
+   */
+  public static function processRadios(&$element, FormStateInterface $form_state, &$complete_form) {
+    $element = parent::processRadios($element, $form_state, $complete_form);
+    foreach ($element['#options'] as $key => $choice) {
+      $element[$key]['#type'] = 'va_gov_form_builder__expanded_radio';
+      $element[$key]['#expanded_content'] = $element['#options_expanded_content'][$key] ?? [];
path: docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadios.php
user: login: ryguyk
body: This is where we grab the content defined in the use of our custom element and pass it to each _individual_ radio element.
created_at: 2025-04-14T19:11:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\va_gov_form_builder\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\va_gov_form_builder\Form\Base\FormBuilderBase;
+
+/**
+ * Form step for choosing a question's response kind.
+ */
+class ResponseKind extends FormBuilderBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'form_builder__response_kind';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['#theme'] = 'form__va_gov_form_builder__response_kind';
+
+    $imageDir = '/modules/custom/va_gov_form_builder/images/';
+    $form['response_kind'] = [
+      '#type' => 'va_gov_form_builder__expanded_radios',
path: docroot/modules/custom/va_gov_form_builder/src/Form/ResponseKind.php
user: login: ryguyk
body: This is where we use our new custom element.
created_at: 2025-04-14T19:12:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\va_gov_form_builder\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\va_gov_form_builder\Form\Base\FormBuilderBase;
+
+/**
+ * Form step for choosing a question's response kind.
+ */
+class ResponseKind extends FormBuilderBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'form_builder__response_kind';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['#theme'] = 'form__va_gov_form_builder__response_kind';
+
+    $imageDir = '/modules/custom/va_gov_form_builder/images/';
+    $form['response_kind'] = [
+      '#type' => 'va_gov_form_builder__expanded_radios',
+      '#title' => $this->t('What kind of response will the submitter provide?'),
+      '#options' => [
+        'choice' => $this->t('Choice'),
+        'date' => $this->t('Date'),
+        'text' => $this->t('Text and/or numbers'),
+      ],
+      '#options_expanded_content' => [
+        'choice' => [
+          '#theme' => 'page_element__va_gov_form_builder__expanded_radio__help_text_optional_image',
+          '#help_text' => $this->t('This allows one or multiple items to be selected from a provided list.'),
+          '#image' => [
+            'alt_text' => 'Example of a list of choices',
+            'caption' => 'Example of a list of choices',
+            'url' => $imageDir . 'response-kind-choice.png',
+          ],
+        ],
+        'date' => [
+          '#theme' => 'page_element__va_gov_form_builder__expanded_radio__help_text_optional_image',
+          '#help_text' => $this->t('This allows a specific or approximate date or date range to be input.'),
+          '#image' => [
+            'alt_text' => 'Example of a date range',
+            'caption' => 'Example of a date range',
+            'url' => $imageDir . 'response-kind-date.png',
+          ],
+        ],
+        'text' => [
+          '#theme' => 'page_element__va_gov_form_builder__expanded_radio__help_text_optional_image',
+          '#help_text' => $this->t('This allows the submitter to fill in the blank for the question you are asking.'),
+          '#image' => [
+            'alt_text' => 'Example of input fields',
+            'caption' => 'Example of input fields',
+            'url' => $imageDir . 'response-kind-text.png',
+          ],
+        ],
+      ],
path: docroot/modules/custom/va_gov_form_builder/src/Form/ResponseKind.php
user: login: ryguyk
body: Here is the additional content to be shown on each radio. 

Note that we use another new template/theme, here, which is a template that defines a specific type of content that is used for expanded radios (the `expanded-radio` template defines how to render the expanded radio in general, while this template defines one specific type of content that we want to use in expanded radios).  We use it on this page, but will also be able to use it on future pages that call for help text but without an image.  

Notably, this template does not _strictly_ relate to expanded radios. It's really just help text with an optional image that could be used anywhere. For now, though, it feels useful to keep the naming specific to expanded radios for clarity, as it's not currently being used anywhere else and wasn't designed with a general use in mind.
created_at: 2025-04-14T19:18:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -844,6 +857,31 @@ public function customOrPredefinedQuestion($nid, $stepParagraphId) {
     return $this->getPage($pageContent, $subtitle, $breadcrumbs, $libraries);
   }
 
+  /**
+   * Response-kind page for custom single-question questions.
+   *
+   * @param string $nid
+   *   The node id of the Digital Form.
+   * @param string $stepParagraphId
+   *   The entity id of the step paragraph.
+   */
+  public function customSingleQuestionResponseKind($nid, $stepParagraphId) {
+    $this->loadDigitalForm($nid);
+    $this->loadStepParagraph($stepParagraphId);
+
+    $formName = 'ResponseKind';
+    $breadcrumbs = $this->generateBreadcrumbs('step.question.custom_or_predefined', 'Response kind');
+    $subtitle = $this->digitalForm->getTitle();
+    $libraries = [
+      'single_column_with_buttons',
+      'response_kind',
+      'expanded_radio',
+      'expanded_radio__help_text_optional_image',
+    ];
path: docroot/modules/custom/va_gov_form_builder/src/Controller/VaGovFormBuilderController.php
user: login: derekhouck
body: I agree with this in principle. Is there any reason a template would need to have different stylesheets when called in different contexts?
created_at: 2025-04-14T19:34:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\va_gov_form_builder\Element;
+
+use Drupal\Core\Render\Element\Radio;
+
+/**
+ * Defines a custom expanded-radio input for a single radio.
+ *
+ * This is an empty class. The definition needs to be here to define
+ * a form element of type `va_gov_form_builder__expanded_radio`,
+ * but the class itself does not need to do anything different than
+ * the parent class. When this form element is called for an individual
+ * radio item of type `va_gov_form_builder__expanded_radios`, it will
+ * be rendered by the appropriate template due to theme suggestions
+ * defined in `hook_theme` of `va_gov_form_builder.module`.
+ *
+ * The template used to render this element is
+ * `va_gov_form_builder/templates/expanded-radio.html.twig`.
+ *
+ * @FormElement("va_gov_form_builder__expanded_radio")
+ */
+class ExpandedRadio extends Radio {
+}
path: docroot/modules/custom/va_gov_form_builder/src/Element/ExpandedRadio.php
user: login: derekhouck
body: I appreciate the explanation.
created_at: 2025-04-14T19:35:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,7 +2,7 @@ services:
   va_gov_form_builder.route_subscriber:
     class: Drupal\va_gov_form_builder\Routing\RouteSubscriber
     tags:
-      - { name: "event_subscriber" }
+      - { name: event_subscriber }
path: docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.services.yml
user: login: derekhouck
body: Is this intentional? Seems a bit out of place compared to the rest of the PR.
created_at: 2025-04-14T19:41:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,7 +2,7 @@ services:
   va_gov_form_builder.route_subscriber:
     class: Drupal\va_gov_form_builder\Routing\RouteSubscriber
     tags:
-      - { name: "event_subscriber" }
+      - { name: event_subscriber }
path: docroot/modules/custom/va_gov_form_builder/va_gov_form_builder.services.yml
user: login: ryguyk
body: Yes, intentional. 

I initially edited this file when I thought that I needed to define services for the custom form elements. When I went looking at other code in the codebase for examples, I didn't find anything that was helpful, but I found that my using of quotes here was not in line with how this was defined anywhere else. So I changed it. Then, I ended up removing the other changes to this file, and this was the only thing left.

So, I agree - this feels a bit odd to be found here. But I did it on purpose and didn't want to open a separate PR for it 😅. Good question! 
created_at: 2025-04-14T19:46:27Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-14T15:58:59Z
body: Checking composer.lock changes...
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T19:19:49Z
body: Checking composer.lock changes...
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T19:20:12Z
body: Checking composer.lock changes...
author_association: NONE
user: login: va-cms-bot
created_at: 2025-04-14T19:56:08Z
body: <!-- Cypress Accessibility Errors -->
## Cypress Accessibility Violations


### `/test-data-odit`
**ID**: `button-name`
**Impact**: critical
**Tags**: `cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT`
**Description**: Ensure buttons have discernible text
**Help**: [Buttons must have discernible text](https://dequeuniversity.com/rules/axe/4.10/button-name?application=axeAPI)
**Nodes**: 
- **HTML**: `<button class="proofing-element-help" role="tooltip" data-proofing-help-title="About 'Page introduction' field" data-proofing-help="Add an introduction that helps visitors understand if information on the page is relevant to them.">          <span aria-hidden="true">i</span>
        </button>`
  **Impact**: critical
  **Target**: `.field--name-field-intro-text-limited-html > .field__label > .proofing-element-help[role="tooltip"]`
  **Summary**: Fix any of the following:
  Element does not have inner text that is visible to screen readers
  aria-label attribute does not exist or is empty
  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
  Element has no title attribute
  Element does not have an implicit (wrapped) &lt;label&gt;
  Element does not have an explicit &lt;label&gt;
  Element's default semantics were not overridden with role=&#34;none&#34; or role=&#34;presentation&#34;


- **HTML**: `<button class="proofing-element-help" role="tooltip" data-proofing-help-title="About 'Generate a table of contents from major headings' field" data-proofing-help="By checking this box, all h2's below this point on the page will be linked with with anchor links. This helps users navigate content on very long pages. Do not check this box unless there is at least 2 h2's on the page.">`
  **Impact**: critical
  **Target**: `.field--name-field-table-of-contents-boolean > .field__label > .proofing-element-help[role="tooltip"]`
  **Summary**: Fix any of the following:
  Element does not have inner text that is visible to screen readers
  aria-label attribute does not exist or is empty
  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
  Element has no title attribute
  Element does not have an implicit (wrapped) &lt;label&gt;
  Element does not have an explicit &lt;label&gt;
  Element's default semantics were not overridden with role=&#34;none&#34; or role=&#34;presentation&#34;


- **HTML**: `<button class="proofing-element-help" role="tooltip" data-proofing-help-title="About 'Main content' field" data-proofing-help="The main body of the page, which appears below the featured content.">          <span aria-hidden="true">i</span>
        </button>`
  **Impact**: critical
  **Target**: `button[data-proofing-help-title="About 'Main content' field"]`
  **Summary**: Fix any of the following:
  Element does not have inner text that is visible to screen readers
  aria-label attribute does not exist or is empty
  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
  Element has no title attribute
  Element does not have an implicit (wrapped) &lt;label&gt;
  Element does not have an explicit &lt;label&gt;
  Element's default semantics were not overridden with role=&#34;none&#34; or role=&#34;presentation&#34;




  
author_association: COLLABORATOR