Verified Commit df73d39e authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2627788 by lauriii, DuaelFr, Pancho, vsujeetkumar, _utsavsharma,...

Issue #2627788 by lauriii, DuaelFr, Pancho, vsujeetkumar, _utsavsharma, KapilV, facine, Danny_Joris, alexpott, juampynr, tim.plunkett: Focus state bug on text field AJAX calls

(cherry picked from commit 27d76911)
parent 66ddcd2a
Loading
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -273,6 +273,12 @@ public static function preRenderAjaxForm($element) {
      $element['#attributes']['data-disable-refocus'] = "true";
    }

    // Add a data attribute to attempt to focus element that was focused before
    // executing ajax commands.
    if ($element['#ajax']['refocus-blur'] ?? FALSE) {
      $element['#attributes']['data-refocus-blur'] = "true";
    }

    // Add a reasonable default event handler if none was specified.
    if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) {
      switch ($element['#type']) {
@@ -327,6 +333,17 @@ public static function preRenderAjaxForm($element) {

    // Attach JavaScript settings to the element.
    if (isset($element['#ajax']['event'])) {
      // By default, focus should return to the element focused prior to the
      // execution of AJAX commands within event listeners attached to the blur
      // event. This behavior can be explicitly overridden if needed.
      if (!isset($element['#ajax']['refocus-blur'])) {
        // The change event on text input types is triggered on blur.
        $text_types = ['password', 'textfield', 'number', 'tel', 'textarea'];
        if ($element['#ajax']['event'] === 'blur' || ($element['#ajax']['event'] === 'change' && in_array($element['#type'], $text_types))) {
          $element['#attributes']['data-refocus-blur'] = "true";
        }
      }

      $element['#attached']['library'][] = 'core/internal.jquery.form';
      $element['#attached']['library'][] = 'core/drupal.ajax';

+34 −12
Original line number Diff line number Diff line
@@ -442,6 +442,13 @@
     */
    this.element = element;

    /**
     * The last focused element right before processing ajax response.
     *
     * @type {string|null}
     */
    this.preCommandsFocusedElementSelector = null;

    /**
     * @type {Drupal.Ajax~elementSettings}
     */
@@ -533,6 +540,7 @@
      },
      beforeSubmit(formValues, elementSettings, options) {
        ajax.ajaxing = true;
        ajax.preCommandsFocusedElementSelector = null;
        return ajax.beforeSubmit(formValues, elementSettings, options);
      },
      beforeSend(xmlhttprequest, options) {
@@ -540,6 +548,9 @@
        return ajax.beforeSend(xmlhttprequest, options);
      },
      success(response, status, xmlhttprequest) {
        ajax.preCommandsFocusedElementSelector =
          document.activeElement.getAttribute('data-drupal-selector');

        // Sanity check for browser support (object expected).
        // When using iFrame uploads, responses must be returned as a string.
        if (typeof response === 'string') {
@@ -1083,20 +1094,31 @@
        // the triggering element or one of its parents if that element does not
        // exist anymore.
        .then(() => {
          if (!focusChanged) {
            let target = false;
            if (this.element) {
              if (
            !focusChanged &&
            this.element &&
            !$(this.element).data('disable-refocus')
                $(this.element).data('refocus-blur') &&
                this.preCommandsFocusedElementSelector
              ) {
                target = document.querySelector(
                  `[data-drupal-selector="${this.preCommandsFocusedElementSelector}"]`,
                );
              }
              if (!target && !$(this.element).data('disable-refocus')) {
                for (
                  let n = elementParents.length - 1;
                  !target && n >= 0;
                  n--
                ) {
            let target = false;

            for (let n = elementParents.length - 1; !target && n >= 0; n--) {
                  target = document.querySelector(
                    `[data-drupal-selector="${elementParents[n].getAttribute(
                      'data-drupal-selector',
                    )}"]`,
                  );
                }
              }
            }
            if (target) {
              $(target).trigger('focus');
            }
+34 −0
Original line number Diff line number Diff line
@@ -111,6 +111,40 @@ public function buildForm(array $form, FormStateInterface $form_state) {
      '#title' => $this->t('Another AJAX checkbox in a nested group'),
    ];

    $form['textfield_focus_tests'] = [
      '#type' => 'details',
      '#title' => $this->t('Test group 2'),
      '#open' => TRUE,
    ];
    $form['textfield_focus_tests']['textfield'] = [
      '#type' => 'textfield',
      '#title' => 'Textfield 1',
      '#ajax' => [
        'callback' => [static::class, 'textfieldCallback'],
      ],
    ];
    $form['textfield_focus_tests']['textfield_2'] = [
      '#type' => 'textfield',
      '#title' => 'Textfield 2',
      '#ajax' => [
        'callback' => [static::class, 'textfieldCallback'],
        'event' => 'change',
        'refocus-blur' => FALSE,
      ],
    ];
    $form['textfield_focus_tests']['textfield_3'] = [
      '#type' => 'textfield',
      '#title' => 'Textfield 3',
      '#ajax' => [
        'callback' => [static::class, 'textfieldCallback'],
        'event' => 'change',
      ],
    ];

    return $form;
  }

  public static function textfieldCallback($form) {
    return $form;
  }

+47 −1
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@ class AjaxTest extends WebDriverTestBase {
  /**
   * {@inheritdoc}
   */
  protected static $modules = ['ajax_test'];
  protected static $modules = ['ajax_test', 'ajax_forms_test'];

  /**
   * {@inheritdoc}
@@ -291,4 +291,50 @@ public function testUiAjaxException() {
    $this->getSession()->executeScript("delete window.jQuery");
  }

  /**
   * Tests ajax focus handling.
   */
  public function testAjaxFocus() {
    $this->drupalGet('/ajax_forms_test_get_form');

    $this->assertNotNull($select = $this->assertSession()->elementExists('css', '#edit-select'));
    $select->setValue('green');
    $this->assertSession()->assertWaitOnAjaxRequest();
    $has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
    $this->assertEquals('edit-select', $has_focus_id);

    $this->assertNotNull($checkbox = $this->assertSession()->elementExists('css', '#edit-checkbox'));
    $checkbox->check();
    $this->assertSession()->assertWaitOnAjaxRequest();
    $has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
    $this->assertEquals('edit-checkbox', $has_focus_id);

    $this->assertNotNull($textfield1 = $this->assertSession()->elementExists('css', '#edit-textfield'));
    $this->assertNotNull($textfield2 = $this->assertSession()->elementExists('css', '#edit-textfield-2'));
    $this->assertNotNull($textfield3 = $this->assertSession()->elementExists('css', '#edit-textfield-3'));

    // Test textfield with 'blur' event listener.
    $textfield1->setValue('Kittens say purr');
    $textfield2->focus();
    $this->assertSession()->assertWaitOnAjaxRequest();
    $has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
    $this->assertEquals('edit-textfield-2', $has_focus_id);

    // Test textfield with 'change' event listener with refocus-blur set to
    // FALSE.
    $textfield2->setValue('Llamas say yarhar');
    $textfield3->focus();
    $this->assertSession()->assertWaitOnAjaxRequest();
    $has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
    $this->assertEquals('edit-textfield-2', $has_focus_id);

    // Test textfield with 'change' event.
    $textfield3->focus();
    $textfield3->setValue('Wasps buzz');
    $textfield3->blur();
    $this->assertSession()->assertWaitOnAjaxRequest();
    $has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id');
    $this->assertEquals('edit-textfield-3', $has_focus_id);
  }

}