Skip to content

Commit

Permalink
should not focus target element if source element was focused on chan…
Browse files Browse the repository at this point in the history
…ge (closes DevExpress#1858) (DevExpress#1857)

* should not focus target element if source element was focused on change

* fix test

* improve test

* rename
  • Loading branch information
AlexKamaev authored and AndreyBelym committed Feb 28, 2019
1 parent 96ae62e commit 816c724
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 11 deletions.
70 changes: 59 additions & 11 deletions src/client/sandbox/event/focus-blur.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import * as styleUtils from '../../utils/style';

const INTERNAL_FOCUS_BLUR_FLAG_PREFIX = 'hammerhead|event|internal-';

const PREVENT_FOCUS_ON_CHANGE = browserUtils.isChrome;

const eventsMap = {
bubbles: {
'focus': 'focusin',
Expand Down Expand Up @@ -115,7 +117,7 @@ export default class FocusBlurSandbox extends SandboxBase {
this._restoreElementNonScrollableParentsScrollState(this.scrollState.elementNonScrollableParentsScrollState);
}

_raiseEvent (el, type, callback, withoutHandlers, isAsync, forMouseEvent, preventScrolling, relatedTarget) {
_raiseEvent (el, type, callback, { withoutHandlers, isAsync, forMouseEvent, preventScrolling, relatedTarget, focusedOnChange } ) {
// NOTE: We cannot use Promise because 'resolve' will be called async, but we need to resolve
// immediately in IE9 and IE10.

Expand Down Expand Up @@ -151,6 +153,16 @@ export default class FocusBlurSandbox extends SandboxBase {
this.eventSimulator[bubblesEventType](el, relatedTarget);
}
}
else if (type === 'focus' && PREVENT_FOCUS_ON_CHANGE) {
const preventFocus = (e, dispatched, preventEvent, cancelHandlers, stopEventPropagation) => {
cancelHandlers();
stopEventPropagation();
};

this.listeners.addInternalEventListener(window, ['focus'], preventFocus);
this.eventSimulator['focus'](el, relatedTarget);
this.listeners.removeInternalEventListener(window, ['focus'], preventFocus);
}

callback();
};
Expand All @@ -173,7 +185,9 @@ export default class FocusBlurSandbox extends SandboxBase {
el[FocusBlurSandbox.getInternalEventFlag(type)] = true;
// NOTE: We should guarantee that activeElement will be changed, therefore we need to call the native
// focus/blur event.
FocusBlurSandbox._getNativeMeth(el, type).call(el);
if (!focusedOnChange)
FocusBlurSandbox._getNativeMeth(el, type).call(el);

this._restoreScrollStateIfNecessary(preventScrolling);

const curDocument = domUtils.findDocument(el);
Expand All @@ -188,14 +202,17 @@ export default class FocusBlurSandbox extends SandboxBase {
// raises page scrolling. We can't prevent it. Therefore, we need to restore a page scrolling value.
const needPreventScrolling = browserUtils.isWebKit || browserUtils.isSafari || browserUtils.isIE;

this._raiseEvent(parentWithTabIndex, 'focus', simulateEvent, false, false, forMouseEvent, needPreventScrolling);
this._raiseEvent(parentWithTabIndex, 'focus', simulateEvent, {
preventScrolling: needPreventScrolling,
forMouseEvent
});
}
// NOTE: Some browsers don't change document.activeElement after calling element.blur() if a browser
// window is in the background. That's why we call body.focus() without handlers. It should be called
// synchronously because client scripts may expect that document.activeElement will be changed immediately
// after element.blur() is called.
else if (type === 'blur' && activeElement === el && el !== curDocument.body)
this._raiseEvent(curDocument.body, 'focus', simulateEvent, true);
this._raiseEvent(curDocument.body, 'focus', simulateEvent, { withoutHandlers: true });
else if (!el.disabled)
simulateEvent();
else
Expand Down Expand Up @@ -272,6 +289,14 @@ export default class FocusBlurSandbox extends SandboxBase {
if (!isCurrentWindowActive && !domUtils.isShadowUIElement(el))
this.activeWindowTracker.makeCurrentWindowActive();

const raiseEventArgs = {
withoutHandlers: withoutHandlers || silent,
isAsync,
forMouseEvent,
preventScrolling,
relatedTarget: activeElement
};

this._raiseEvent(el, 'focus', () => {
if (!silent)
this.elementEditingWatcher.watchElementEditing(el);
Expand All @@ -280,11 +305,11 @@ export default class FocusBlurSandbox extends SandboxBase {
// specify document.active for this iframe manually, so we call focus without handlers.
if (isElementInIframe && iframeElement &&
domUtils.getActiveElement(this.topWindow.document) !== iframeElement)
this._raiseEvent(iframeElement, 'focus', () => this._raiseSelectionChange(callback, el), true, isAsync);
this._raiseEvent(iframeElement, 'focus', () => this._raiseSelectionChange(callback, el), { withoutHandlers: true, isAsync });
else
this._raiseSelectionChange(callback, el);

}, withoutHandlers || silent, isAsync, forMouseEvent, preventScrolling, activeElement);
}, raiseEventArgs);
};

if (isNativeFocus && browserUtils.isIE) {
Expand Down Expand Up @@ -338,11 +363,13 @@ export default class FocusBlurSandbox extends SandboxBase {
this.blur(domUtils.getIframeByElement(activeElement), raiseFocusEvent, true, isNativeFocus);
}
else if (needBlur) {
this.blur(activeElement, () => {
this.blur(activeElement, focusOnChange => {
if (needBlurIframe)
this.blur(domUtils.getIframeByElement(activeElement), raiseFocusEvent, true, isNativeFocus);
else
else if (!focusOnChange)
raiseFocusEvent();
else if (typeof callback === 'function')
callback();
}, silent, isNativeFocus, el);
}
else
Expand All @@ -352,7 +379,8 @@ export default class FocusBlurSandbox extends SandboxBase {
}

blur (el, callback, withoutHandlers, isNativeBlur, relatedTarget) {
const activeElement = domUtils.getActiveElement(domUtils.findDocument(el));
const curDocument = domUtils.findDocument(el);
const activeElement = domUtils.getActiveElement(curDocument);
// NOTE: In IE, if you call the focus() or blur() method from script, an active element is changed
// immediately but events are raised asynchronously after some timeout (in MSEdgethe focus/blur methods
// are executed synchronously).
Expand All @@ -361,15 +389,35 @@ export default class FocusBlurSandbox extends SandboxBase {
if (activeElement !== el)
withoutHandlers = true;

let focusedOnChange = false;

if (!withoutHandlers) {
const focusOnChangeHandler = e => {
focusedOnChange = e.target === el;
};

if (PREVENT_FOCUS_ON_CHANGE)
this.listeners.addInternalEventListener(window, ['focus'], focusOnChangeHandler);

this.elementEditingWatcher.processElementChanging(el);

if (PREVENT_FOCUS_ON_CHANGE)
this.listeners.removeInternalEventListener(window, ['focus'], focusOnChangeHandler);

this.elementEditingWatcher.stopWatching(el);
}

const raiseEventParameters = {
withoutHandlers,
isAsync,
relatedTarget,
focusedOnChange
};

this._raiseEvent(el, 'blur', () => {
if (typeof callback === 'function')
callback();
}, withoutHandlers, isAsync, false, false, relatedTarget);
callback(focusedOnChange);
}, raiseEventParameters);
}

static _processFocusPseudoClassSelector (selector) {
Expand Down
105 changes: 105 additions & 0 deletions test/client/fixtures/sandbox/event/focus-blur-change-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var styleUtil = hammerhead.utils.style;
var activeWindowTracker = hammerhead.sandbox.event.focusBlur.activeWindowTracker;
var eventSimulator = hammerhead.sandbox.event.eventSimulator;
var focusBlur = hammerhead.sandbox.event.focusBlur;
var focusBlurSandbox = hammerhead.eventSandbox.focusBlur;
var nativeMethods = hammerhead.nativeMethods;
var elementEditingWatcher = hammerhead.sandbox.event.elementEditingWatcher;
var iframeSandbox = hammerhead.sandbox.iframe;
Expand Down Expand Up @@ -1320,3 +1321,107 @@ test('focus should not raise an error in IE11 when is called with element within
ok(true);
});
});


asyncTest('Should not change active element after source element was focused on change', function () {
var firstInput = document.createElement('input');
var secondInput = document.createElement('input');
var firstNativeInput = nativeMethods.createElement.call(document, 'input');
var secondNativeInput = nativeMethods.createElement.call(document, 'input');

var callbackCalled = false;
var changeCalled = false;
var changeNativeCalled = false;
var secondInputWasFocused = false;
var secondNativeInputWasFocused = false;

var firstNativeInputChangeHandler = function () {
changeNativeCalled = true;

firstNativeInput.focus();
};

var firstInputChangeHandler = function () {
changeCalled = true;

firstInput.focus();
};

var secondInputFocusHandler = function () {
secondInputWasFocused = true;
};

var secondNativeInputFocusHandler = function () {
secondNativeInputWasFocused = true;
};

function nextTick () {
return new Promise(function (resolve) {
setTimeout(resolve, 100);
});
}

firstInput.className = TEST_ELEMENT_CLASS;
secondInput.className = TEST_ELEMENT_CLASS;
firstNativeInput.className = TEST_ELEMENT_CLASS;
secondNativeInput.className = TEST_ELEMENT_CLASS;

firstInput.addEventListener('change', firstInputChangeHandler);
secondInput.addEventListener('focus', secondInputFocusHandler);

firstNativeInput.addEventListener('change', firstNativeInputChangeHandler);
secondNativeInput.addEventListener('focus', secondNativeInputFocusHandler);

document.body.appendChild(firstInput);
document.body.appendChild(secondInput);

document.body.appendChild(firstNativeInput);
document.body.appendChild(secondNativeInput);

var expectedNativeElement = firstNativeInput;
var expectedElement = firstInput;
var expectedSecondNativeInputFocused = false;
var expectedSecondInputFocused = false;

if (!browserUtils.isChrome) {
expectedNativeElement = secondNativeInput;
expectedElement = secondInput;
expectedSecondNativeInputFocused = true;
expectedSecondInputFocused = true;
}

firstNativeInput.focus();

firstNativeInput.value = '1';

secondNativeInput.focus();

return nextTick()
.then(function () {
ok(changeNativeCalled);
equal(document.activeElement, expectedNativeElement);
equal(secondNativeInputWasFocused, expectedSecondNativeInputFocused);

firstInput.focus();

firstInput.value = '1';

focusBlurSandbox.focus(secondInput, function () {
callbackCalled = true;
});
})
.then(function () {
return nextTick();
})
.then(function () {
ok(changeCalled);
equal(document.activeElement, expectedElement);
equal(secondInputWasFocused, expectedSecondInputFocused);
equal(callbackCalled, true);

removeTestElements();

startNext();
});
});

0 comments on commit 816c724

Please sign in to comment.