diff --git a/lib/autoResize/autoResize.js b/lib/autoResize/autoResize.js index 1b0f6267dd4..14c573b1702 100644 --- a/lib/autoResize/autoResize.js +++ b/lib/autoResize/autoResize.js @@ -145,6 +145,9 @@ function autoResize() { observe(el, 'drop', delayedResize); observe(el, 'keydown', delayedResize); observe(el, 'focus', resize); + observe(el, 'compositionstart', delayedResize); + observe(el, 'compositionupdate', delayedResize); + observe(el, 'compositionend', delayedResize); } resize(); @@ -165,6 +168,9 @@ function autoResize() { unObserve(el, 'drop', delayedResize); unObserve(el, 'keydown', delayedResize); unObserve(el, 'focus', resize); + unObserve(el, 'compositionstart', delayedResize); + unObserve(el, 'compositionupdate', delayedResize); + unObserve(el, 'compositionend', delayedResize); }, resize: resize }; diff --git a/src/core.js b/src/core.js index 3ec4d04e889..f202127af4d 100644 --- a/src/core.js +++ b/src/core.js @@ -1220,10 +1220,11 @@ export default function Core(rootElement, userSettings, rootInstanceSymbol = fal * * @memberof Core# * @function destroyEditor - * @param {Boolean} [revertOriginal] If != `true`, edited data is saved. Otherwise the previous value is restored. + * @param {Boolean} [revertOriginal=false] If `true`, the previous value will be restored. Otherwise, the edited value will be saved. + * @param {Boolean} [prepareEditorIfNeeded=true] If `true` the editor under the selected cell will be prepared to open. */ - this.destroyEditor = function(revertOriginal) { - instance._refreshBorders(revertOriginal); + this.destroyEditor = function(revertOriginal = false, prepareEditorIfNeeded = true) { + instance._refreshBorders(revertOriginal, prepareEditorIfNeeded); }; /** @@ -3544,13 +3545,14 @@ export default function Core(rootElement, userSettings, rootInstanceSymbol = fal * Refresh selection borders. This is temporary method relic after selection rewrite. * * @private - * @param {Boolean} revertOriginal + * @param {Boolean} [revertOriginal=false] If `true`, the previous value will be restored. Otherwise, the edited value will be saved. + * @param {Boolean} [prepareEditorIfNeeded=true] If `true` the editor under the selected cell will be prepared to open. */ - this._refreshBorders = function(revertOriginal) { + this._refreshBorders = function(revertOriginal = false, prepareEditorIfNeeded = true) { editorManager.destroyEditor(revertOriginal); instance.view.render(); - if (selection.isSelected()) { + if (prepareEditorIfNeeded && selection.isSelected()) { editorManager.prepareEditor(); } }; diff --git a/src/editorManager.js b/src/editorManager.js index 64f6275c39f..fbcdcb3aa48 100644 --- a/src/editorManager.js +++ b/src/editorManager.js @@ -253,7 +253,7 @@ function EditorManager(instance, priv, selection) { // Open editor when text composition is started (IME editor) eventManager.addEventListener(document.documentElement, 'compositionstart', (event) => { - if (!destroyed && activeEditor && !activeEditor.isOpened()) { + if (!destroyed && activeEditor && !activeEditor.isOpened() && instance.isListening()) { _this.openEditor('', event); } }); diff --git a/src/editors/textEditor.js b/src/editors/textEditor.js index 958f4b44659..b1cc5834cf9 100644 --- a/src/editors/textEditor.js +++ b/src/editors/textEditor.js @@ -41,39 +41,39 @@ TextEditor.prototype.init = function() { }; TextEditor.prototype.prepare = function(row, col, prop, td, originalValue, cellProperties) { + const previousState = this.state; + BaseEditor.prototype.prepare.apply(this, arguments); if (!cellProperties.readOnly) { this.refreshDimensions(true); - if (cellProperties.allowInvalid) { + const { + allowInvalid, + fragmentSelection, + } = cellProperties; + + if (allowInvalid) { this.TEXTAREA.value = ''; // Remove an empty space from texarea (added by copyPaste plugin to make copy/paste functionality work with IME) } - if (isMSBrowser()) { - // Move textarea element out off the viewport due to the cursor overlapping bug on IE. + if (previousState !== EditorState.FINISHED) { this.hideEditableElement(); } - // @TODO: The fragmentSelection functionality is conflicted with IME. To make fragmentSelection working below is a condition which disables - // IME when fragmentSelection is enabled - if (!cellProperties.fragmentSelection) { + + // @TODO: The fragmentSelection functionality is conflicted with IME. For this feature refocus has to + // be disabled (to make IME working). + const restoreFocus = !fragmentSelection; + + if (restoreFocus) { this.instance._registerImmediate(() => this.focus()); } } }; TextEditor.prototype.hideEditableElement = function() { - // IE and Edge have the bug where the caret of the editable elements (eg. input, texarea) is always visible - // despite the element is overlapped by another element. To hide element we need to move element out of the viewport. - if (isMSBrowser()) { - this.textareaParentStyle.top = '-9999px'; - this.textareaParentStyle.left = '-9999px'; - } else { - // For other browsers hide element under Handsontable itself. - this.textareaParentStyle.top = '0px'; - this.textareaParentStyle.left = '0px'; - } - + this.textareaParentStyle.top = '-9999px'; + this.textareaParentStyle.left = '-9999px'; this.textareaParentStyle.zIndex = '-1'; }; @@ -90,6 +90,10 @@ TextEditor.prototype.setValue = function(newValue) { }; TextEditor.prototype.beginEditing = function(newInitialValue, event) { + if (this.state !== EditorState.VIRGIN) { + return; + } + this.TEXTAREA.value = ''; // Remove an empty space from texarea (added by copyPaste plugin to make copy/paste functionality work with IME). BaseEditor.prototype.beginEditing.apply(this, arguments); }; @@ -215,6 +219,7 @@ TextEditor.prototype.focus = function() { TextEditor.prototype.createElements = function() { this.TEXTAREA = document.createElement('TEXTAREA'); + this.TEXTAREA.tabIndex = -1; addClass(this.TEXTAREA, 'handsontableInput'); diff --git a/src/tableView.js b/src/tableView.js index de08c32b85a..80cb7512388 100644 --- a/src/tableView.js +++ b/src/tableView.js @@ -140,7 +140,7 @@ function TableView(instance) { if (outsideClickDeselects) { instance.deselectCell(); } else { - instance.destroyEditor(); + instance.destroyEditor(false, false); } }); diff --git a/test/e2e/Core_selection.spec.js b/test/e2e/Core_selection.spec.js index 8c7688ef07b..ed26aeb272d 100644 --- a/test/e2e/Core_selection.spec.js +++ b/test/e2e/Core_selection.spec.js @@ -77,206 +77,6 @@ describe('Core_selection', () => { expect(getSelected()).toBeUndefined(); }); - it('should not deselect the currently selected cell after clicking on a scrollbar', () => { - var hot = handsontable({ - outsideClickDeselects: false, - minRows: 20, - minCols: 2, - width: 400, - height: 100 - }); - selectCell(0, 0); - - var holderBoundingBox = hot.view.wt.wtTable.holder.getBoundingClientRect(), - verticalScrollbarCoords = { - x: holderBoundingBox.left + holderBoundingBox.width - 3, - y: holderBoundingBox.top + (holderBoundingBox.height / 2) - }, - horizontalScrollbarCoords = { - x: holderBoundingBox.left + (holderBoundingBox.width / 2), - y: holderBoundingBox.top + holderBoundingBox.height - 3 - }; - - $(hot.view.wt.wtTable.holder).simulate('mousedown', { - clientX: verticalScrollbarCoords.x, - clientY: verticalScrollbarCoords.y - }); - - expect(getSelected()).toEqual([[0, 0, 0, 0]]); - - $(hot.view.wt.wtTable.holder).simulate('mousedown', { - clientX: horizontalScrollbarCoords.x, - clientY: horizontalScrollbarCoords.y - }); - - expect(getSelected()).toEqual([[0, 0, 0, 0]]); - }); - - it('should not deselect currently selected cell', () => { - handsontable({ - outsideClickDeselects: false - }); - selectCell(0, 0); - - $('html').simulate('mousedown'); - - expect(getSelected()).toEqual([[0, 0, 0, 0]]); - }); - - it('should allow to focus on external input and hold current selection informations', () => { - var textarea = $('').prependTo($('body')); - - handsontable({ - outsideClickDeselects: false - }); - selectCell(0, 0); - - textarea.simulate('mousedown'); - textarea.focus(); - - expect(document.activeElement.id).toEqual('test_textarea'); - expect(getSelected()).toEqual([[0, 0, 0, 0]]); - textarea.remove(); - }); - - it('should allow to type in external input while holding current selection information', () => { - var textarea = $('').prependTo($('body')); - var keyPressed; - handsontable({ - outsideClickDeselects: false - }); - selectCell(0, 0); - - textarea.focus(); - textarea.simulate('mousedown'); - textarea.simulate('mouseup'); - - textarea.on('keydown', (event) => { - keyPressed = event.keyCode; - }); - - var LETTER_A_KEY = 97; - - $(document.activeElement).simulate('keydown', { - keyCode: LETTER_A_KEY - }); - - // textarea should receive the event and be an active element - expect(keyPressed).toEqual(LETTER_A_KEY); - expect(document.activeElement).toBe(document.getElementById('test_textarea')); - - // should preserve selection, close editor and save changes - expect(getSelected()).toEqual([[0, 0, 0, 0]]); - expect(getDataAtCell(0, 0)).toBeNull(); - - textarea.remove(); - }); - - it('should allow to type in external input after opening cell editor', () => { - var textarea = $('').prependTo($('body')); - var keyPressed; - handsontable({ - outsideClickDeselects: false - }); - selectCell(0, 0); - keyDown('enter'); - document.activeElement.value = 'Foo'; - - textarea.focus(); - textarea.simulate('mousedown'); - textarea.simulate('mouseup'); - - textarea.on('keydown', (event) => { - keyPressed = event.keyCode; - }); - - var LETTER_A_KEY = 97; - - $(document.activeElement).simulate('keydown', { - keyCode: LETTER_A_KEY - }); - - // textarea should receive the event and be an active element - expect(keyPressed).toEqual(LETTER_A_KEY); - expect(document.activeElement).toBe(document.getElementById('test_textarea')); - - // should preserve selection, close editor and save changes - expect(getSelected()).toEqual([[0, 0, 0, 0]]); - expect(getDataAtCell(0, 0)).toEqual('Foo'); - - textarea.remove(); - }); - - it('should deselect on outside click if outsideClickDeselects is a function that returns true', () => { - var textarea = $('').prependTo($('body')); - var keyPressed; - handsontable({ - outsideClickDeselects: () => true, - }); - selectCell(0, 0); - keyDown('enter'); - document.activeElement.value = 'Foo'; - - textarea.focus(); - textarea.simulate('mousedown'); - textarea.simulate('mouseup'); - - textarea.on('keydown', (event) => { - keyPressed = event.keyCode; - }); - - var LETTER_A_KEY = 97; - - $(document.activeElement).simulate('keydown', { - keyCode: LETTER_A_KEY - }); - - // textarea should receive the event and be an active element - expect(keyPressed).toEqual(LETTER_A_KEY); - expect(document.activeElement).toBe(document.getElementById('test_textarea')); - - // should NOT preserve selection - expect(getSelected()).toBeUndefined(); - expect(getDataAtCell(0, 0)).toEqual('Foo'); - - textarea.remove(); - }); - - it('should not deselect on outside click if outsideClickDeselects is a function that returns false', () => { - var textarea = $('').prependTo($('body')); - var keyPressed; - handsontable({ - outsideClickDeselects: () => false, - }); - selectCell(0, 0); - keyDown('enter'); - document.activeElement.value = 'Foo'; - - textarea.focus(); - textarea.simulate('mousedown'); - textarea.simulate('mouseup'); - - textarea.on('keydown', (event) => { - keyPressed = event.keyCode; - }); - - var LETTER_A_KEY = 97; - - $(document.activeElement).simulate('keydown', { - keyCode: LETTER_A_KEY - }); - - // textarea should receive the event and be an active element - expect(keyPressed).toEqual(LETTER_A_KEY); - expect(document.activeElement).toBe(document.getElementById('test_textarea')); - - // should preserve selection, close editor and save changes - expect(getSelected()).toEqual([[0, 0, 0, 0]]); - expect(getDataAtCell(0, 0)).toEqual('Foo'); - - textarea.remove(); - }); - it('should fix start range if provided is out of bounds (to the left)', () => { handsontable({ startRows: 5, diff --git a/test/e2e/Core_validate.spec.js b/test/e2e/Core_validate.spec.js index a028e5e0086..c6e5e1bdc2f 100644 --- a/test/e2e/Core_validate.spec.js +++ b/test/e2e/Core_validate.spec.js @@ -1472,18 +1472,20 @@ describe('Core_validate', () => { }, 300); }); - it('should listen to key changes after cell is corrected (allowInvalid: false)', (done) => { - var onAfterValidate = jasmine.createSpy('onAfterValidate'); + it('should listen to key changes after cell is corrected (allowInvalid: false)', async () => { + const onAfterValidate = jasmine.createSpy('onAfterValidate'); handsontable({ data: arrayOfObjects(), allowInvalid: false, columns: [ - {data: 'id', + { + data: 'id', type: 'numeric', validator(val, cb) { cb(parseInt(val, 10) > 100); - }}, + } + }, {data: 'name'}, {data: 'lastName'} ], @@ -1498,22 +1500,21 @@ describe('Core_validate', () => { keyDownUp('enter'); // should be ignored - setTimeout(() => { - expect(isEditorVisible()).toBe(true); - document.activeElement.value = '999'; + await sleep(200); - onAfterValidate.calls.reset(); - keyDownUp('enter'); // should be accepted - }, 200); + expect(isEditorVisible()).toBe(true); + document.activeElement.value = '999'; - setTimeout(() => { - expect(isEditorVisible()).toBe(false); - expect(getSelected()).toEqual([[3, 0, 3, 0]]); + onAfterValidate.calls.reset(); + keyDownUp('enter'); // should be accepted - keyDownUp('arrow_up'); - expect(getSelected()).toEqual([[2, 0, 2, 0]]); - done(); - }, 400); + await sleep(200); + + expect(isEditorVisible()).toBe(false); + expect(getSelected()).toEqual([[3, 0, 3, 0]]); + + keyDownUp('arrow_up'); + expect(getSelected()).toEqual([[2, 0, 2, 0]]); }); it('should allow keyboard movement when cell is being validated (move DOWN)', async () => { diff --git a/test/e2e/editors/baseEditor.spec.js b/test/e2e/editors/baseEditor.spec.js index 7d1b801f14e..71d9b5af6ee 100644 --- a/test/e2e/editors/baseEditor.spec.js +++ b/test/e2e/editors/baseEditor.spec.js @@ -84,4 +84,35 @@ describe('BaseEditor', () => { expect(Handsontable.editors.SelectEditor).toBeDefined(); expect(Handsontable.editors.TextEditor).toBeDefined(); }); + + describe('IME support', () => { + it('should not throw an error when composition is started in multiple instances environment', async () => { + const errorSpy = jasmine.createSpyObj('error', ['test']); + const prevError = window.onerror; + + window.onerror = errorSpy.test; + + const hot1 = handsontable({}); + const container2 = $(`
`).appendTo('body'); + const hot2 = container2.handsontable().handsontable('getInstance'); + + $(hot1.getCell(1, 1)).simulate('mousedown'); + $(hot1.getCell(1, 1)).simulate('mouseover'); + $(hot1.getCell(1, 1)).simulate('mouseup'); + + document.documentElement.dispatchEvent(new CompositionEvent('compositionstart')); + + $(hot2.getCell(1, 1)).simulate('mousedown'); + $(hot2.getCell(1, 1)).simulate('mouseover'); + $(hot2.getCell(1, 1)).simulate('mouseup'); + + document.documentElement.dispatchEvent(new CompositionEvent('compositionstart')); + + expect(errorSpy.test).not.toHaveBeenCalled(); + + hot2.destroy(); + container2.remove(); + window.onerror = prevError; + }); + }); }); diff --git a/test/e2e/editors/textEditor.spec.js b/test/e2e/editors/textEditor.spec.js index 031a37937f2..5291805764b 100644 --- a/test/e2e/editors/textEditor.spec.js +++ b/test/e2e/editors/textEditor.spec.js @@ -63,6 +63,17 @@ describe('TextEditor', () => { expect(keyProxy().val()).toEqual('string'); }); + it('should render textarea editor with tabindex=-1 attribute', async () => { + const hot = handsontable(); + + selectCell(0, 0); + keyDown('enter'); + + await sleep(10); + + expect(hot.getActiveEditor().TEXTAREA.getAttribute('tabindex')).toBe('-1'); + }); + it('should render textarea editor in specified size at cell 0, 0 without headers', (done) => { var hot = handsontable(); @@ -564,7 +575,6 @@ describe('TextEditor', () => { }); it('editor size should not exceed the viewport after text edit', function() { - handsontable({ data: Handsontable.helper.createSpreadsheetData(10, 5), width: 200, @@ -585,7 +595,6 @@ describe('TextEditor', () => { expect($textarea.offset().left + $textarea.outerWidth()).not.toBeGreaterThan($wtHider.offset().left + this.$container.outerWidth()); expect($textarea.offset().top + $textarea.outerHeight()).not.toBeGreaterThan($wtHider.offset().top + $wtHider.outerHeight()); - }); it('should open editor after selecting cell in another table and hitting enter', function() { @@ -1067,7 +1076,7 @@ describe('TextEditor', () => { }, 150); }); - // Input element can not lose the focus while entering new characters. It breaks IME editor functionality for Asian users. + // Input element can not lose the focus while entering new characters. It breaks IME editor functionality. it('should not lose the focus on input element while inserting new characters (#839)', async () => { let blured = false; const listener = () => { @@ -1107,6 +1116,46 @@ describe('TextEditor', () => { done(); }); + it('should keep editor open, focusable and with untouched value when allowInvalid is set as false', async () => { + handsontable({ + data: Handsontable.helper.createSpreadsheetData(5, 5), + allowInvalid: false, + validator(val, cb) { + cb(false); + }, + }); + selectCell(0, 0); + + keyDown('enter'); + destroyEditor(); + document.activeElement.value = '999'; + + await sleep(10); + + expect(document.activeElement).toBe(getActiveEditor().TEXTAREA); + expect(isEditorVisible()).toBe(true); + expect(getActiveEditor().TEXTAREA.value).toBe('999'); + + keyDown('enter'); + + expect(document.activeElement).toBe(getActiveEditor().TEXTAREA); + expect(isEditorVisible()).toBe(true); + expect(getActiveEditor().TEXTAREA.value).toBe('999'); + + const cell = $(getCell(1, 1)); + + mouseDown(cell); + mouseUp(cell); + mouseDown(cell); + mouseUp(cell); + + await sleep(10); + + expect(document.activeElement).toBe(getActiveEditor().TEXTAREA); + expect(isEditorVisible()).toBe(true); + expect(getActiveEditor().TEXTAREA.value).toBe('999'); + }); + describe('IME support', () => { it('should focus editable element after selecting the cell', async () => { handsontable({ @@ -1118,5 +1167,28 @@ describe('TextEditor', () => { expect(document.activeElement).toBe(getActiveEditor().TEXTAREA); }); + + it('editor size should change after composition started', async () => { + handsontable({ + data: Handsontable.helper.createSpreadsheetData(10, 5), + width: 400, + height: 400, + }); + + selectCell(2, 2); + keyDownUp('enter'); + + const textarea = getActiveEditor().TEXTAREA; + + textarea.value = '测试, 测试, 测试, 测试, 测试'; + textarea.dispatchEvent(new CompositionEvent('compositionstart')); // Trigger textarea resize + textarea.dispatchEvent(new CompositionEvent('compositionupdate')); // Trigger textarea resize + textarea.dispatchEvent(new CompositionEvent('compositionend')); // Trigger textarea resize + + await sleep(100); + + expect($(textarea).width()).toBe(244); + expect($(textarea).height()).toBe(23); + }); }); }); diff --git a/test/e2e/settings/outsideClickDeselects.spec.js b/test/e2e/settings/outsideClickDeselects.spec.js new file mode 100644 index 00000000000..937d5b4d5f5 --- /dev/null +++ b/test/e2e/settings/outsideClickDeselects.spec.js @@ -0,0 +1,360 @@ +describe('settings', () => { + describe('outsideClickDeselects', () => { + const id = 'testContainer'; + + beforeEach(function() { + this.$container = $(`
`).appendTo('body'); + }); + + afterEach(function() { + if (this.$container) { + destroy(); + this.$container.remove(); + } + }); + + it('should not deselect the currently selected cell after clicking on a scrollbar', () => { + const hot = handsontable({ + outsideClickDeselects: false, + minRows: 20, + minCols: 2, + width: 400, + height: 100 + }); + selectCell(0, 0); + + const holderBoundingBox = hot.view.wt.wtTable.holder.getBoundingClientRect(), + verticalScrollbarCoords = { + x: holderBoundingBox.left + holderBoundingBox.width - 3, + y: holderBoundingBox.top + (holderBoundingBox.height / 2) + }, + horizontalScrollbarCoords = { + x: holderBoundingBox.left + (holderBoundingBox.width / 2), + y: holderBoundingBox.top + holderBoundingBox.height - 3 + }; + + $(hot.view.wt.wtTable.holder).simulate('mousedown', { + clientX: verticalScrollbarCoords.x, + clientY: verticalScrollbarCoords.y + }); + + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + + $(hot.view.wt.wtTable.holder).simulate('mousedown', { + clientX: horizontalScrollbarCoords.x, + clientY: horizontalScrollbarCoords.y + }); + + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + }); + + it('should not deselect currently selected cell', () => { + handsontable({ + outsideClickDeselects: false + }); + selectCell(0, 0); + + $('html').simulate('mousedown'); + + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + }); + + it('should not deselect currently selected cell (outsideClickDeselects as function)', () => { + handsontable({ + outsideClickDeselects: () => false + }); + selectCell(0, 0); + + $('html').simulate('mousedown'); + + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + }); + + it('should deselect currently selected cell', () => { + handsontable({ + outsideClickDeselects: true + }); + selectCell(0, 0); + + $('html').simulate('mousedown'); + + expect(getSelected()).toBeUndefined(); + }); + + it('should deselect currently selected cell (outsideClickDeselects as function)', () => { + handsontable({ + outsideClickDeselects: () => true + }); + selectCell(0, 0); + + $('html').simulate('mousedown'); + + expect(getSelected()).toBeUndefined(); + }); + + it('should allow to focus on external input when outsideClickDeselects is set as true', async () => { + const textarea = $('').prependTo($('body')); + + handsontable({ + outsideClickDeselects: true + }); + selectCell(0, 0); + + // It is necessary to fire event simulation in the next event loop cycle due to the autofocus editable element in setImmediate function. + await sleep(0); + + textarea.simulate('mousedown'); + textarea.focus(); + + expect(document.activeElement).toBe(textarea[0]); + + await sleep(50); + + expect(document.activeElement).toBe(textarea[0]); + + textarea.remove(); + }); + + it('should allow to focus on external input when outsideClickDeselects is set as true (outsideClickDeselects as function)', async () => { + const textarea = $('').prependTo($('body')); + + handsontable({ + outsideClickDeselects: () => true + }); + selectCell(0, 0); + + await sleep(0); + + textarea.simulate('mousedown'); + textarea.focus(); + + expect(document.activeElement).toBe(textarea[0]); + + await sleep(50); + + expect(document.activeElement).toBe(textarea[0]); + + textarea.remove(); + }); + + it('should allow to focus on external input when outsideClickDeselects is set as false', async () => { + const textarea = $('').prependTo($('body')); + + handsontable({ + outsideClickDeselects: false + }); + selectCell(0, 0); + + await sleep(0); + + textarea.simulate('mousedown'); + textarea.focus(); + + expect(document.activeElement).toBe(textarea[0]); + + await sleep(50); + + expect(document.activeElement).toBe(textarea[0]); + + textarea.remove(); + }); + + it('should allow to focus on external input when outsideClickDeselects is set as false (outsideClickDeselects as function)', async () => { + const textarea = $('').prependTo($('body')); + + handsontable({ + outsideClickDeselects: () => false + }); + selectCell(0, 0); + + await sleep(0); + + textarea.simulate('mousedown'); + textarea.focus(); + + expect(document.activeElement).toBe(textarea[0]); + + await sleep(50); + + expect(document.activeElement).toBe(textarea[0]); + + textarea.remove(); + }); + + it('should allow to type in external input while holding current selection information', async () => { + const textarea = $('').prependTo($('body')); + let keyPressed; + + handsontable({ + outsideClickDeselects: false + }); + selectCell(0, 0); + + textarea.focus(); + textarea.simulate('mousedown'); + textarea.simulate('mouseup'); + + textarea.on('keydown', (event) => { + keyPressed = event.keyCode; + }); + + const LETTER_A_KEY = 97; + + $(document.activeElement).simulate('keydown', { + keyCode: LETTER_A_KEY + }); + + // textarea should receive the event and be an active element + expect(keyPressed).toEqual(LETTER_A_KEY); + expect(document.activeElement).toBe(textarea[0]); + + // should preserve selection, close editor and save changes + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + expect(getDataAtCell(0, 0)).toBeNull(); + + await sleep(50); + + $(document.activeElement).simulate('keydown', { + keyCode: LETTER_A_KEY + }); + + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + expect(getDataAtCell(0, 0)).toBeNull(); + + textarea.remove(); + }); + + it('should allow to type in external input while holding current selection information (outsideClickDeselects as function)', async () => { + const textarea = $('').prependTo($('body')); + let keyPressed; + + handsontable({ + outsideClickDeselects: () => false + }); + selectCell(0, 0); + + textarea.focus(); + textarea.simulate('mousedown'); + textarea.simulate('mouseup'); + + textarea.on('keydown', (event) => { + keyPressed = event.keyCode; + }); + + const LETTER_A_KEY = 97; + + $(document.activeElement).simulate('keydown', { + keyCode: LETTER_A_KEY + }); + + // textarea should receive the event and be an active element + expect(keyPressed).toEqual(LETTER_A_KEY); + expect(document.activeElement).toBe(textarea[0]); + + // should preserve selection, close editor and save changes + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + expect(getDataAtCell(0, 0)).toBeNull(); + + await sleep(50); + + $(document.activeElement).simulate('keydown', { + keyCode: LETTER_A_KEY + }); + + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + expect(getDataAtCell(0, 0)).toBeNull(); + + textarea.remove(); + }); + + xit('should allow to type in external input after opening cell editor', async () => { + const textarea = $('').prependTo($('body')); + let keyPressed; + + handsontable({ + outsideClickDeselects: false + }); + selectCell(0, 0); + keyDown('enter'); + document.activeElement.value = 'Foo'; + + textarea.focus(); + textarea.simulate('mousedown'); + textarea.simulate('mouseup'); + + textarea.on('keydown', (event) => { + keyPressed = event.keyCode; + }); + + const LETTER_A_KEY = 97; + + $(document.activeElement).simulate('keydown', { + keyCode: LETTER_A_KEY + }); + + // textarea should receive the event and be an active element + expect(keyPressed).toEqual(LETTER_A_KEY); + expect(document.activeElement).toBe(textarea[0]); + + // should preserve selection, close editor and save changes + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + expect(getDataAtCell(0, 0)).toEqual('Foo'); + + await sleep(50); + + $(document.activeElement).simulate('keydown', { + keyCode: LETTER_A_KEY + }); + + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + expect(getDataAtCell(0, 0)).toEqual('Foo'); + + textarea.remove(); + }); + + it('should allow to type in external input after opening cell editor (outsideClickDeselects as function)', async () => { + const textarea = $('').prependTo($('body')); + let keyPressed; + + handsontable({ + outsideClickDeselects: () => false + }); + selectCell(0, 0); + keyDown('enter'); + document.activeElement.value = 'Foo'; + + textarea.focus(); + textarea.simulate('mousedown'); + textarea.simulate('mouseup'); + + textarea.on('keydown', (event) => { + keyPressed = event.keyCode; + }); + + const LETTER_A_KEY = 97; + + $(document.activeElement).simulate('keydown', { + keyCode: LETTER_A_KEY + }); + + // textarea should receive the event and be an active element + expect(keyPressed).toEqual(LETTER_A_KEY); + expect(document.activeElement).toBe(textarea[0]); + + // should preserve selection, close editor and save changes + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + expect(getDataAtCell(0, 0)).toEqual('Foo'); + + await sleep(50); + + $(document.activeElement).simulate('keydown', { + keyCode: LETTER_A_KEY + }); + + expect(getSelected()).toEqual([[0, 0, 0, 0]]); + expect(getDataAtCell(0, 0)).toEqual('Foo'); + + textarea.remove(); + }); + }); +}); diff --git a/test/helpers/common.js b/test/helpers/common.js index e5312efc3c4..45179e635e2 100644 --- a/test/helpers/common.js +++ b/test/helpers/common.js @@ -71,7 +71,12 @@ export function isEditorVisible(editableElement) { const keyProxyHolder = (editableElement || keyProxy()).parent(); - return keyProxyHolder.size() > 0 ? keyProxyHolder.css('z-index') !== '-1' : false; + if (keyProxyHolder.size() === 0) { + return false; + } + const css = (cssProp) => keyProxyHolder.css(cssProp); + + return css('z-index') !== '-1' && css('top') !== '-9999px' && css('left') !== '-9999px'; }; export function isFillHandleVisible() {