Skip to content

Commit 55b37ec

Browse files
marker-daomarker dao ®
andauthored
FileUploader: Attach Enter keyup handler to select file dialog button (T1256752)
Co-authored-by: marker dao ® <[email protected]>
1 parent 762f7ae commit 55b37ec

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
@@ -16,7 +16,7 @@ import { isDefined, isFunction, isNumeric } from '@js/core/utils/type';
1616
import { getWindow } from '@js/core/utils/window';
1717
import { name as clickEventName } from '@js/events/click';
1818
import eventsEngine from '@js/events/core/events_engine';
19-
import { addNamespace, isTouchEvent } from '@js/events/utils/index';
19+
import { addNamespace, isTouchEvent, normalizeKeyName } from '@js/events/utils/index';
2020
import messageLocalization from '@js/localization/message';
2121
import type { ButtonStyle, ButtonType } from '@js/ui/button';
2222
import Button from '@js/ui/button';
@@ -59,6 +59,13 @@ const FILEUPLOADER_AFTER_LOAD_DELAY = 400;
5959
const FILEUPLOADER_CHUNK_META_DATA_NAME = 'chunkMetadata';
6060
const DRAG_EVENT_DELTA = 1;
6161

62+
const DIALOG_TRIGGER_EVENT_NAMESPACE = 'dxFileUploaderDialogTrigger';
63+
64+
const keyUpEventName = 'keyup';
65+
66+
const ENTER_KEY = 'enter';
67+
const SPACE_KEY = 'space';
68+
6269
let renderFileUploaderInput = () => $('<input>').attr('type', 'file');
6370
// @ts-expect-error
6471
const isFormDataSupported = () => !!window.FormData;
@@ -69,6 +76,8 @@ export interface Properties extends PublicProperties {
6976
_uploadButtonType?: ButtonType;
7077
}
7178

79+
type FileDialogEventTarget = dxElementWrapper | Properties['dialogTrigger'];
80+
7281
class FileUploader extends Editor<Properties> {
7382
// Temporary solution. Move to component level
7483
public NAME!: string;
@@ -106,8 +115,6 @@ class FileUploader extends Editor<Properties> {
106115

107116
_totalFilesSize?: any;
108117

109-
_selectFileDialogHandler?: any;
110-
111118
_isCustomClickEvent?: any;
112119

113120
_progressAction?: any;
@@ -826,19 +833,21 @@ class FileUploader extends Editor<Properties> {
826833
disabled: this.option('readOnly'),
827834
hoverStateEnabled: this.option('hoverStateEnabled'),
828835
});
829-
this._selectFileDialogHandler = this._selectButtonClickHandler.bind(this);
830836

831837
// NOTE: click triggering on input 'file' works correctly only in native click handler when device is used
832838
if (devices.real().deviceType === 'desktop') {
833-
this._selectButton.option('onClick', this._selectFileDialogHandler);
839+
this._selectButton.option('onClick', () => this._selectFileDialogClickHandler());
834840
} else {
835-
this._attachSelectFileDialogHandler(this._selectButton.$element());
841+
this._attachSelectFileDialogHandlers(this._selectButton.$element());
836842
}
837-
this._attachSelectFileDialogHandler(this.option('dialogTrigger'));
843+
844+
const { dialogTrigger } = this.option();
845+
846+
this._attachSelectFileDialogHandlers(dialogTrigger);
838847
}
839848

840849
// @ts-expect-error
841-
_selectButtonClickHandler() {
850+
_selectFileDialogClickHandler() {
842851
if (this.option('useNativeInputClick')) {
843852
return;
844853
}
@@ -853,19 +862,35 @@ class FileUploader extends Editor<Properties> {
853862
this._isCustomClickEvent = false;
854863
}
855864

856-
_attachSelectFileDialogHandler(target) {
865+
_attachSelectFileDialogHandlers(target: FileDialogEventTarget): void {
857866
if (!isDefined(target)) {
858867
return;
859868
}
860-
this._detachSelectFileDialogHandler(target);
861-
eventsEngine.on($(target), 'click', this._selectFileDialogHandler);
869+
870+
this._detachSelectFileDialogHandlers(target);
871+
872+
const $target = $(target);
873+
874+
eventsEngine.on($target, addNamespace(clickEventName, DIALOG_TRIGGER_EVENT_NAMESPACE), () => {
875+
this._selectFileDialogClickHandler();
876+
});
877+
eventsEngine.on($target, addNamespace(keyUpEventName, DIALOG_TRIGGER_EVENT_NAMESPACE), (e: KeyboardEvent) => {
878+
const normalizedKeyName = normalizeKeyName(e);
879+
880+
if (normalizedKeyName === ENTER_KEY || normalizedKeyName === SPACE_KEY) {
881+
this._selectFileDialogClickHandler();
882+
}
883+
});
862884
}
863885

864-
_detachSelectFileDialogHandler(target) {
886+
_detachSelectFileDialogHandlers(target: FileDialogEventTarget): void {
865887
if (!isDefined(target)) {
866888
return;
867889
}
868-
eventsEngine.off($(target), 'click', this._selectFileDialogHandler);
890+
891+
const $target = $(target);
892+
893+
eventsEngine.off($target, `.${DIALOG_TRIGGER_EVENT_NAMESPACE}`);
869894
}
870895

871896
_renderUploadButton() {
@@ -1143,7 +1168,10 @@ class FileUploader extends Editor<Properties> {
11431168
this._$fileInput.detach();
11441169
// @ts-expect-error
11451170
delete this._$filesContainer;
1146-
this._detachSelectFileDialogHandler(this.option('dialogTrigger'));
1171+
1172+
const { dialogTrigger } = this.option();
1173+
1174+
this._detachSelectFileDialogHandlers(dialogTrigger);
11471175
this._detachDragEventHandlers(this.option('dropZone'));
11481176

11491177
if (this._files) {
@@ -1426,8 +1454,8 @@ class FileUploader extends Editor<Properties> {
14261454
});
14271455
break;
14281456
case 'dialogTrigger':
1429-
this._detachSelectFileDialogHandler(previousValue);
1430-
this._attachSelectFileDialogHandler(value);
1457+
this._detachSelectFileDialogHandlers(previousValue);
1458+
this._attachSelectFileDialogHandlers(value);
14311459
break;
14321460
case 'dropZone':
14331461
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 'core/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)