Skip to content

Commit 1a534d3

Browse files
authored
FileUploader: Attach Enter keyup handler to select file dialog button (T1256752)
1 parent 11cf015 commit 1a534d3

File tree

3 files changed

+97
-67
lines changed

3 files changed

+97
-67
lines changed

packages/devextreme/js/__internal/ui/m_file_uploader.ts

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-classes-per-file */
22
import { name as clickEventName } from '@js/common/core/events/click';
33
import eventsEngine from '@js/common/core/events/core/events_engine';
4-
import { addNamespace, isTouchEvent } from '@js/common/core/events/utils/index';
4+
import { addNamespace, isTouchEvent, normalizeKeyName } from '@js/common/core/events/utils/index';
55
import messageLocalization from '@js/common/core/localization/message';
66
import registerComponent from '@js/core/component_registrator';
77
import devices from '@js/core/devices';
@@ -60,6 +60,13 @@ const FILEUPLOADER_AFTER_LOAD_DELAY = 400;
6060
const FILEUPLOADER_CHUNK_META_DATA_NAME = 'chunkMetadata';
6161
const DRAG_EVENT_DELTA = 1;
6262

63+
const DIALOG_TRIGGER_EVENT_NAMESPACE = 'dxFileUploaderDialogTrigger';
64+
65+
const keyUpEventName = 'keyup';
66+
67+
const ENTER_KEY = 'enter';
68+
const SPACE_KEY = 'space';
69+
6370
let renderFileUploaderInput = () => $('<input>').attr('type', 'file');
6471
// @ts-expect-error
6572
const isFormDataSupported = () => !!window.FormData;
@@ -73,6 +80,8 @@ export interface Properties extends PublicProperties {
7380
interface FileUploaderProperties extends Properties,
7481
Omit<EditorProperties<FileUploader>, UnresolvedEvents | 'value'> {}
7582

83+
type FileDialogEventTarget = dxElementWrapper | FileUploaderProperties['dialogTrigger'];
84+
7685
class FileUploader extends Editor<FileUploaderProperties> {
7786
// Temporary solution. Move to component level
7887
public NAME!: string;
@@ -110,8 +119,6 @@ class FileUploader extends Editor<FileUploaderProperties> {
110119

111120
_totalFilesSize?: any;
112121

113-
_selectFileDialogHandler?: any;
114-
115122
_isCustomClickEvent?: any;
116123

117124
_progressAction?: any;
@@ -834,19 +841,21 @@ class FileUploader extends Editor<FileUploaderProperties> {
834841
disabled: this.option('readOnly'),
835842
hoverStateEnabled: this.option('hoverStateEnabled'),
836843
});
837-
this._selectFileDialogHandler = this._selectButtonClickHandler.bind(this);
838844

839845
// NOTE: click triggering on input 'file' works correctly only in native click handler when device is used
840846
if (devices.real().deviceType === 'desktop') {
841-
this._selectButton.option('onClick', this._selectFileDialogHandler);
847+
this._selectButton.option('onClick', () => this._selectFileDialogClickHandler());
842848
} else {
843-
this._attachSelectFileDialogHandler(this._selectButton.$element());
849+
this._attachSelectFileDialogHandlers(this._selectButton.$element());
844850
}
845-
this._attachSelectFileDialogHandler(this.option('dialogTrigger'));
851+
852+
const { dialogTrigger } = this.option();
853+
854+
this._attachSelectFileDialogHandlers(dialogTrigger);
846855
}
847856

848857
// @ts-expect-error
849-
_selectButtonClickHandler() {
858+
_selectFileDialogClickHandler() {
850859
if (this.option('useNativeInputClick')) {
851860
return;
852861
}
@@ -861,19 +870,35 @@ class FileUploader extends Editor<FileUploaderProperties> {
861870
this._isCustomClickEvent = false;
862871
}
863872

864-
_attachSelectFileDialogHandler(target) {
873+
_attachSelectFileDialogHandlers(target: FileDialogEventTarget): void {
865874
if (!isDefined(target)) {
866875
return;
867876
}
868-
this._detachSelectFileDialogHandler(target);
869-
eventsEngine.on($(target), 'click', this._selectFileDialogHandler);
877+
878+
this._detachSelectFileDialogHandlers(target);
879+
880+
const $target = $(target);
881+
882+
eventsEngine.on($target, addNamespace(clickEventName, DIALOG_TRIGGER_EVENT_NAMESPACE), () => {
883+
this._selectFileDialogClickHandler();
884+
});
885+
eventsEngine.on($target, addNamespace(keyUpEventName, DIALOG_TRIGGER_EVENT_NAMESPACE), (e: KeyboardEvent) => {
886+
const normalizedKeyName = normalizeKeyName(e);
887+
888+
if (normalizedKeyName === ENTER_KEY || normalizedKeyName === SPACE_KEY) {
889+
this._selectFileDialogClickHandler();
890+
}
891+
});
870892
}
871893

872-
_detachSelectFileDialogHandler(target) {
894+
_detachSelectFileDialogHandlers(target: FileDialogEventTarget): void {
873895
if (!isDefined(target)) {
874896
return;
875897
}
876-
eventsEngine.off($(target), 'click', this._selectFileDialogHandler);
898+
899+
const $target = $(target);
900+
901+
eventsEngine.off($target, `.${DIALOG_TRIGGER_EVENT_NAMESPACE}`);
877902
}
878903

879904
_renderUploadButton() {
@@ -1152,7 +1177,10 @@ class FileUploader extends Editor<FileUploaderProperties> {
11521177
this._$fileInput.detach();
11531178
// @ts-expect-error
11541179
delete this._$filesContainer;
1155-
this._detachSelectFileDialogHandler(this.option('dialogTrigger'));
1180+
1181+
const { dialogTrigger } = this.option();
1182+
1183+
this._detachSelectFileDialogHandlers(dialogTrigger);
11561184
this._detachDragEventHandlers(this.option('dropZone'));
11571185

11581186
if (this._files) {
@@ -1439,8 +1467,8 @@ class FileUploader extends Editor<FileUploaderProperties> {
14391467
});
14401468
break;
14411469
case 'dialogTrigger':
1442-
this._detachSelectFileDialogHandler(previousValue);
1443-
this._attachSelectFileDialogHandler(value);
1470+
this._detachSelectFileDialogHandlers(previousValue);
1471+
this._attachSelectFileDialogHandlers(value);
14441472
break;
14451473
case 'dropZone':
14461474
this._detachDragEventHandlers(previousValue);

packages/devextreme/js/ui/file_manager/ui.file_manager.file_uploader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class FileManagerFileUploader extends Widget {
7575
tryUpload() {
7676
const info = this._findAndUpdateAvailableUploaderInfo();
7777
if(info) {
78-
info.fileUploader._selectButtonClickHandler();
78+
info.fileUploader._selectFileDialogClickHandler();
7979
}
8080
}
8181

packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/fileUploader.tests.js

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import $ from 'jquery';
22
import FileUploader from 'ui/file_uploader';
3+
import 'ui/drop_down_button';
4+
import 'ui/button_group';
35
import devices from '__internal/core/m_devices';
46
import { Deferred } from 'core/utils/deferred';
57
import keyboardMock from '../../helpers/keyboardMock.js';
@@ -20,6 +22,7 @@ QUnit.testStart(function() {
2022
const internals = FileUploader.__internals;
2123

2224
const FILEUPLOADER_EMPTY_CLASS = 'dx-fileuploader-empty';
25+
const FILEUPLOADER_INVALID_CLASS = 'dx-fileuploader-invalid';
2326

2427
const FILEUPLOADER_CONTENT_CLASS = 'dx-fileuploader-content';
2528
const FILEUPLOADER_INPUT_WRAPPER_CLASS = 'dx-fileuploader-input-wrapper';
@@ -38,7 +41,7 @@ const FILEUPLOADER_CANCEL_BUTTON_CLASS = 'dx-fileuploader-cancel-button';
3841
const FILEUPLOADER_UPLOAD_BUTTON_CLASS = 'dx-fileuploader-upload-button';
3942
const FILEUPLOADER_FILE_STATUS_MESSAGE_CLASS = 'dx-fileuploader-file-status-message';
4043

41-
const FILEUPLOADER_INVALID_CLASS = 'dx-fileuploader-invalid';
44+
const BUTTON_GROUP_CLASS = 'dx-buttongroup';
4245

4346
const FILEUPLOADER_AFTER_LOAD_DELAY = 500;
4447

@@ -836,16 +839,16 @@ QUnit.module('custom uploading', moduleConfig, () => {
836839

837840
QUnit.test('set custom dialog trigger attaches click event handler on targets', function(assert) {
838841
const instance = $('#fileuploader').dxFileUploader().dxFileUploader('instance');
839-
sinon.stub(instance, '_attachSelectFileDialogHandler');
842+
sinon.stub(instance, '_attachSelectFileDialogHandlers');
840843

841844
instance.option('dialogTrigger', '.pic');
842845

843-
assert.strictEqual(instance._attachSelectFileDialogHandler.callCount, 1, 'attachHandlers method called');
844-
const items = instance._attachSelectFileDialogHandler.args[0];
846+
assert.strictEqual(instance._attachSelectFileDialogHandlers.callCount, 1, 'attachHandlers method called');
847+
const items = instance._attachSelectFileDialogHandlers.args[0];
845848
assert.strictEqual(items.length, 1, 'attachHandlers args is valid');
846849
assert.strictEqual(items[0], '.pic', 'attachHandlers args is valid');
847850

848-
instance._attachSelectFileDialogHandler.restore();
851+
instance._attachSelectFileDialogHandlers.restore();
849852
});
850853

851854
QUnit.test('it is possible to drop files using custom dropzone', function(assert) {
@@ -2039,8 +2042,8 @@ QUnit.module('option change', moduleConfig, () => {
20392042
const customDropZone = $('<div>').addClass('dropZone').appendTo('#qunit-fixture');
20402043
const customDialogTrigger = $('<div>').addClass('trigger').appendTo('#qunit-fixture');
20412044
const instance = $('#fileuploader').dxFileUploader().dxFileUploader('instance');
2042-
sinon.stub(instance, '_attachSelectFileDialogHandler');
2043-
sinon.stub(instance, '_detachSelectFileDialogHandler');
2045+
sinon.stub(instance, '_attachSelectFileDialogHandlers');
2046+
sinon.stub(instance, '_detachSelectFileDialogHandlers');
20442047
sinon.stub(instance, '_attachDragEventHandlers');
20452048
sinon.stub(instance, '_detachDragEventHandlers');
20462049

@@ -2050,13 +2053,13 @@ QUnit.module('option change', moduleConfig, () => {
20502053
dropZone: '.dropZone'
20512054
});
20522055
this.clock.tick(100);
2053-
instance._detachSelectFileDialogHandler.reset();
2056+
instance._detachSelectFileDialogHandlers.reset();
20542057
instance._detachDragEventHandlers.reset();
20552058

2056-
assert.ok(instance._attachSelectFileDialogHandler.callCount >= 1, '_attachSelectFileDialogHandler method called');
2057-
let items = instance._attachSelectFileDialogHandler.args[0];
2058-
assert.strictEqual(items.length, 1, '_attachSelectFileDialogHandler args is valid');
2059-
assert.strictEqual(items[0], '.trigger', '_attachSelectFileDialogHandler args is valid');
2059+
assert.ok(instance._attachSelectFileDialogHandlers.callCount >= 1, '_attachSelectFileDialogHandlers method called');
2060+
let items = instance._attachSelectFileDialogHandlers.args[0];
2061+
assert.strictEqual(items.length, 1, '_attachSelectFileDialogHandlers args is valid');
2062+
assert.strictEqual(items[0], '.trigger', '_attachSelectFileDialogHandlers args is valid');
20602063

20612064
assert.ok(instance._attachDragEventHandlers.callCount >= 1, '_attachDragEventHandlers method called');
20622065
items = instance._attachDragEventHandlers.args[0];
@@ -2065,18 +2068,18 @@ QUnit.module('option change', moduleConfig, () => {
20652068

20662069
instance.dispose();
20672070

2068-
assert.strictEqual(instance._detachSelectFileDialogHandler.callCount, 1, '_detachSelectFileDialogHandler method called');
2069-
items = instance._detachSelectFileDialogHandler.args[0] || [];
2070-
assert.strictEqual(items.length, 1, '_detachSelectFileDialogHandler args is valid');
2071-
assert.strictEqual(items[0], '.trigger', '_detachSelectFileDialogHandler args is valid');
2071+
assert.strictEqual(instance._detachSelectFileDialogHandlers.callCount, 1, '_detachSelectFileDialogHandlers method called');
2072+
items = instance._detachSelectFileDialogHandlers.args[0] || [];
2073+
assert.strictEqual(items.length, 1, '_detachSelectFileDialogHandlers args is valid');
2074+
assert.strictEqual(items[0], '.trigger', '_detachSelectFileDialogHandlers args is valid');
20722075

20732076
assert.strictEqual(instance._detachDragEventHandlers.callCount, 1, '_detachDragEventHandlers method called');
20742077
items = instance._detachDragEventHandlers.args[0] || [];
20752078
assert.strictEqual(items.length, 1, '_detachDragEventHandlers args is valid');
20762079
assert.strictEqual(items[0], '.dropZone', '_detachDragEventHandlers args is valid');
20772080

2078-
instance._attachSelectFileDialogHandler.restore();
2079-
instance._detachSelectFileDialogHandler.restore();
2081+
instance._attachSelectFileDialogHandlers.restore();
2082+
instance._detachSelectFileDialogHandlers.restore();
20802083
instance._attachDragEventHandlers.restore();
20812084
instance._detachDragEventHandlers.restore();
20822085
customDropZone.remove();
@@ -4200,17 +4203,21 @@ QUnit.module('readOnly option', moduleConfig, () => {
42004203
assert.ok($cancelButtons.eq(1).hasClass('dx-state-disabled'), '2nd button is disabled');
42014204
});
42024205

4203-
QUnit.test('dialogTrigger should be unable to call _selectButtonClickHandler', function(assert) {
4204-
const instance = $('#fileuploader').dxFileUploader({
4206+
QUnit.test('File selection does not open if component is read-only', function(assert) {
4207+
const $element = $('#fileuploader').dxFileUploader({
42054208
readOnly: true,
4206-
uploadMode: 'useButtons'
4207-
}).dxFileUploader('instance');
4208-
sinon.stub(instance, '_selectButtonClickHandler').callsFake(() => instance._selectFileDialogHandler());
4209+
uploadMode: 'useButtons',
4210+
});
4211+
const instance = $element.dxFileUploader('instance');
4212+
4213+
instance._selectFileDialogClickHandler();
4214+
4215+
const fileUploaderInputClickSpy = sinon.spy();
4216+
const $fileUploaderInput = $element.find(`.${FILEUPLOADER_INPUT_CLASS}`);
42094217

4210-
instance._selectButtonClickHandler();
4211-
assert.strictEqual(instance._selectButtonClickHandler.returnValues[0], false, 'selectFile method not called');
4218+
$fileUploaderInput.on('click', fileUploaderInputClickSpy);
42124219

4213-
instance._selectButtonClickHandler.restore();
4220+
assert.strictEqual(fileUploaderInputClickSpy.callCount, 0, 'selectFile method not called');
42144221
});
42154222

42164223
QUnit.test('uploading events can be fired (successful upload)', function(assert) {
@@ -4355,38 +4362,33 @@ QUnit.module('readOnly option', moduleConfig, () => {
43554362
$fileUploader.find('.dx-fileuploader-input-wrapper').trigger('dragenter');
43564363
assert.notOk($fileUploader.hasClass('dx-fileuploader-dragover'), 'drag event was not handled for input wrapper element');
43574364
});
4358-
43594365
});
43604366

4361-
QUnit.module('dxButton integration', moduleConfig, () => {
4362-
QUnit.test('dialog should be shown after press enter key on dxButton (T1178836)', function(assert) {
4363-
if(devices.real().deviceType !== 'desktop') {
4364-
assert.ok(true, 'keyboard is not supported for not generic devices');
4365-
return;
4366-
}
4367+
QUnit.module('integration of dx button components via dialogTrigger', moduleConfig, () => {
4368+
['enter', 'space'].forEach(keyName => {
4369+
['dxButton', 'dxButtonGroup', 'dxDropDownButton'].forEach(component => {
4370+
QUnit.test(`dialog should be shown after press ${keyName} key on ${component} (T1178836, T1256752)`, function(assert) {
4371+
if(devices.real().deviceType !== 'desktop') {
4372+
assert.ok(true, 'keyboard is not supported for not generic devices');
4373+
return;
4374+
}
43674375

4368-
const $customDialogTrigger = $('<div>').appendTo('#qunit-fixture');
4376+
const $dialogTrigger = $('<div>')[component]().appendTo('#qunit-fixture');
43694377

4370-
$customDialogTrigger.dxButton({
4371-
text: 'button'
4372-
});
4378+
$('#fileuploader').dxFileUploader({ dialogTrigger: $dialogTrigger });
43734379

4374-
const instance = $('#fileuploader').dxFileUploader({
4375-
dialogTrigger: $customDialogTrigger,
4376-
visible: false
4377-
}).dxFileUploader('instance');
4378-
const spy = sinon.spy();
4380+
const $fileUploaderInput = $(`.${FILEUPLOADER_INPUT_CLASS}`);
4381+
const $focusTarget = component === 'dxButton' ? $dialogTrigger : $(`.${BUTTON_GROUP_CLASS}`);
43794382

4380-
$(`.${FILEUPLOADER_INPUT_CLASS}`).on('click', spy);
4383+
const fileUploaderInputClickSpy = sinon.spy();
4384+
const keyboard = keyboardMock($focusTarget);
43814385

4382-
instance.option({
4383-
uploadMode: 'useButtons',
4384-
});
4385-
assert.strictEqual(spy.callCount, 0, 'click on input not fired');
4386+
$fileUploaderInput.on('click', fileUploaderInputClickSpy);
43864387

4387-
const keyboard = keyboardMock($customDialogTrigger);
4388-
keyboard.keyDown('enter');
4388+
keyboard.keyUp(keyName);
43894389

4390-
assert.strictEqual(spy.callCount, 1, 'click on input fired');
4390+
assert.strictEqual(fileUploaderInputClickSpy.calledOnce, true, 'click on input fired once');
4391+
});
4392+
});
43914393
});
43924394
});

0 commit comments

Comments
 (0)