From 1e0c6a48c61756528a02c2992804f559ebe86d16 Mon Sep 17 00:00:00 2001 From: Olga Torkhanova Date: Mon, 24 May 2021 14:59:33 +0300 Subject: [PATCH] Fire blur on click/dbl-click/tap on non-focusable element The root cause of the issue is that the blur event is not triggered on the current active element in the __focus__ helper in case if the clicked (double-clicked, tapped) element is not focusable. If the clicked (double-clicked, tapped) element is focusable the previous active element gets blur event trough the __focus__ helper. A call to __blur__ was added to the __focus__ helper for the case when the clicked (double-clicked, tapped) element element is not focusable. --- .../@ember/test-helpers/dom/click.ts | 6 +- .../@ember/test-helpers/dom/double-click.ts | 5 +- .../@ember/test-helpers/dom/focus.ts | 28 +++++-- tests/unit/dom/click-test.js | 41 +++++++++++ tests/unit/dom/double-click-test.js | 73 +++++++++++++++++++ tests/unit/dom/tap-test.js | 61 ++++++++++++++++ 6 files changed, 200 insertions(+), 14 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/click.ts b/addon-test-support/@ember/test-helpers/dom/click.ts index 25655c435..fd4b2da97 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.ts +++ b/addon-test-support/@ember/test-helpers/dom/click.ts @@ -3,12 +3,12 @@ import { getWindowOrElement } from './-get-window-or-element'; import fireEvent from './fire-event'; import { __focus__ } from './focus'; import settled from '../settled'; -import isFocusable from './-is-focusable'; import { Promise } from '../-utils'; import isFormControl from './-is-form-control'; -import Target from './-target'; +import Target, { isWindow } from './-target'; import { log } from '@ember/test-helpers/dom/-logging'; import { runHooks, registerHook } from '../-internal/helper-hooks'; +import { __blur__ } from './blur'; const PRIMARY_BUTTON = 1; const MAIN_BUTTON_PRESSED = 0; @@ -34,7 +34,7 @@ export const DEFAULT_CLICK_OPTIONS = { export function __click__(element: Element | Document | Window, options: MouseEventInit): void { fireEvent(element, 'mousedown', options); - if (isFocusable(element)) { + if (!isWindow(element)) { __focus__(element); } diff --git a/addon-test-support/@ember/test-helpers/dom/double-click.ts b/addon-test-support/@ember/test-helpers/dom/double-click.ts index 48bec3396..de2ae67ac 100644 --- a/addon-test-support/@ember/test-helpers/dom/double-click.ts +++ b/addon-test-support/@ember/test-helpers/dom/double-click.ts @@ -3,10 +3,9 @@ import { getWindowOrElement } from './-get-window-or-element'; import fireEvent from './fire-event'; import { __focus__ } from './focus'; import settled from '../settled'; -import isFocusable from './-is-focusable'; import { Promise } from '../-utils'; import { DEFAULT_CLICK_OPTIONS } from './click'; -import Target from './-target'; +import Target, { isWindow } from './-target'; import { log } from '@ember/test-helpers/dom/-logging'; import isFormControl from './-is-form-control'; import { runHooks, registerHook } from '../-internal/helper-hooks'; @@ -26,7 +25,7 @@ export function __doubleClick__( ): void { fireEvent(element, 'mousedown', options); - if (isFocusable(element)) { + if (!isWindow(element)) { __focus__(element); } diff --git a/addon-test-support/@ember/test-helpers/dom/focus.ts b/addon-test-support/@ember/test-helpers/dom/focus.ts index 91766dbd9..4bcfab0ee 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.ts +++ b/addon-test-support/@ember/test-helpers/dom/focus.ts @@ -17,21 +17,29 @@ registerHook('focus', 'start', (target: Target) => { @param {Element} element the element to trigger events on */ export function __focus__(element: HTMLElement | Element | Document | SVGElement): void { + const previousFocusedElement = + document.activeElement && + document.activeElement !== element && + isFocusable(document.activeElement) + ? document.activeElement + : null; + + // fire __blur__ manually with the null relatedTarget when the target is not focusable + // and there was a previously focused element if (!isFocusable(element)) { - throw new Error(`${element} is not focusable`); + if (previousFocusedElement) { + __blur__(previousFocusedElement, null); + } + + return; } let browserIsNotFocused = document.hasFocus && !document.hasFocus(); // fire __blur__ manually with the correct relatedTarget when the browser is not // already in focus and there was a previously focused element - if ( - document.activeElement && - document.activeElement !== element && - isFocusable(document.activeElement) && - browserIsNotFocused - ) { - __blur__(document.activeElement, element); + if (previousFocusedElement && browserIsNotFocused) { + __blur__(previousFocusedElement, element); } // makes `document.activeElement` be `element`. If the browser is focused, it also fires a focus event @@ -88,6 +96,10 @@ export default function focus(target: Target): Promise { throw new Error(`Element not found when calling \`focus('${target}')\`.`); } + if (!isFocusable(element)) { + throw new Error(`${element} is not focusable`); + } + __focus__(element); return settled(); diff --git a/tests/unit/dom/click-test.js b/tests/unit/dom/click-test.js index 8b2d3c684..d83b69208 100644 --- a/tests/unit/dom/click-test.js +++ b/tests/unit/dom/click-test.js @@ -233,6 +233,47 @@ module('DOM Helper: click', function (hooks) { assert.verifySteps(['mousedown', 'mouseup', 'click']); }); }); + + module('focusable and non-focusable elements interaction', function () { + test('clicking on non-focusable element triggers blur on active element', async function (assert) { + element = document.createElement('div'); + + insertElement(element); + + const focusableElement = buildInstrumentedElement('input'); + + await click(focusableElement); + await click(element); + + assert.verifySteps(['mousedown', 'focus', 'focusin', 'mouseup', 'click', 'blur', 'focusout']); + }); + + test('clicking on focusable element triggers blur on active element', async function (assert) { + element = document.createElement('input'); + + insertElement(element); + + const focusableElement = buildInstrumentedElement('input'); + + await click(focusableElement); + await click(element); + + assert.verifySteps(['mousedown', 'focus', 'focusin', 'mouseup', 'click', 'blur', 'focusout']); + }); + + test('clicking on non-focusable element does not trigger blur on non-focusable active element', async function (assert) { + element = document.createElement('div'); + + insertElement(element); + + const nonFocusableElement = buildInstrumentedElement('div'); + + await click(nonFocusableElement); + await click(element); + + assert.verifySteps(['mousedown', 'mouseup', 'click']); + }); + }); }); module('DOM Helper: click with window', function () { diff --git a/tests/unit/dom/double-click-test.js b/tests/unit/dom/double-click-test.js index 59023b7e9..fbb65d9c5 100644 --- a/tests/unit/dom/double-click-test.js +++ b/tests/unit/dom/double-click-test.js @@ -277,6 +277,79 @@ module('DOM Helper: doubleClick', function (hooks) { ]); }); }); + + module('focusable and non-focusable elements interaction', function () { + test('cdouble-licking on non-focusable element triggers blur on active element', async function (assert) { + element = document.createElement('div'); + + insertElement(element); + + const focusableElement = buildInstrumentedElement('input'); + + await doubleClick(focusableElement); + await doubleClick(element); + + assert.verifySteps([ + 'mousedown', + 'focus', + 'focusin', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + 'blur', + 'focusout', + ]); + }); + + test('double-clicking on focusable element triggers blur on active element', async function (assert) { + element = document.createElement('input'); + + insertElement(element); + + const focusableElement = buildInstrumentedElement('input'); + + await doubleClick(focusableElement); + await doubleClick(element); + + assert.verifySteps([ + 'mousedown', + 'focus', + 'focusin', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + 'blur', + 'focusout', + ]); + }); + + test('double-clicking on non-focusable element does not trigger blur on non-focusable active element', async function (assert) { + element = document.createElement('div'); + + insertElement(element); + + const nonFocusableElement = buildInstrumentedElement('div'); + + await doubleClick(nonFocusableElement); + await doubleClick(element); + + assert.verifySteps([ + 'mousedown', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]); + }); + }); }); module('DOM Helper: doubleClick with window', function () { diff --git a/tests/unit/dom/tap-test.js b/tests/unit/dom/tap-test.js index a38bc60ed..f5f70dee6 100644 --- a/tests/unit/dom/tap-test.js +++ b/tests/unit/dom/tap-test.js @@ -181,4 +181,65 @@ module('DOM Helper: tap', function (hooks) { assert.rejects(tap(element), new Error('Can not `tap` disabled [object HTMLInputElement]')); }); }); + + module('focusable and non-focusable elements interaction', function () { + test('tapping on non-focusable element triggers blur on active element', async function (assert) { + element = document.createElement('div'); + + insertElement(element); + + const focusableElement = buildInstrumentedElement('input'); + + await tap(focusableElement); + await tap(element); + + assert.verifySteps([ + 'touchstart', + 'touchend', + 'mousedown', + 'focus', + 'focusin', + 'mouseup', + 'click', + 'blur', + 'focusout', + ]); + }); + + test('tapping on focusable element triggers blur on active element', async function (assert) { + element = document.createElement('input'); + + insertElement(element); + + const focusableElement = buildInstrumentedElement('input'); + + await tap(focusableElement); + await tap(element); + + assert.verifySteps([ + 'touchstart', + 'touchend', + 'mousedown', + 'focus', + 'focusin', + 'mouseup', + 'click', + 'blur', + 'focusout', + ]); + }); + + test('tapping on non-focusable element does not trigger blur on non-focusable active element', async function (assert) { + element = document.createElement('div'); + + insertElement(element); + + const nonFocusableElement = buildInstrumentedElement('div'); + + await tap(nonFocusableElement); + await tap(element); + + assert.verifySteps(['touchstart', 'touchend', 'mousedown', 'mouseup', 'click']); + }); + }); });