Skip to content

Commit 8eafd07

Browse files
committed
* FileUploadInput: add design doc
* FileUploadInput: add file type validation for DnD * FileUploadInput: update unit tests * FileUploader: add formatted file size display * FileUploader: add CustomValidator example
1 parent 0f74a09 commit 8eafd07

File tree

14 files changed

+214
-100
lines changed

14 files changed

+214
-100
lines changed

src/examples/src/config.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import LabelledFileUploadInput from './widgets/file-upload-input/Labelled';
6565
import MultipleFileUploadInput from './widgets/file-upload-input/Multiple';
6666
import NoDropFileUploadInput from './widgets/file-upload-input/NoDrop';
6767
import BasicFileUploader from './widgets/file-uploader/Basic';
68+
import CustomValidatorFileUploader from './widgets/file-uploader/CustomValidator';
6869
import DisabledFileUploader from './widgets/file-uploader/Disabled';
6970
import MultipleFileUploader from './widgets/file-uploader/Multiple';
7071
import ValidatedFileUploader from './widgets/file-uploader/Validated';
@@ -763,6 +764,11 @@ export const config = {
763764
title: 'Validated FileUploader',
764765
filename: 'Validated',
765766
module: ValidatedFileUploader
767+
},
768+
{
769+
title: 'FileUploader with custom validator',
770+
filename: 'CustomValidator',
771+
module: CustomValidatorFileUploader
766772
}
767773
]
768774
},
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { create, tsx } from '@dojo/framework/core/vdom';
2+
import FileUploader from '@dojo/widgets/file-uploader';
3+
import Example from '../../Example';
4+
5+
const factory = create();
6+
7+
export default factory(function CustomValidator() {
8+
function validateName(file: File) {
9+
if (file.name === 'validfile.txt') {
10+
return { valid: true };
11+
} else {
12+
return {
13+
message: 'File name must be "validfile.txt"',
14+
valid: false
15+
};
16+
}
17+
}
18+
19+
return (
20+
<Example>
21+
<FileUploader customValidator={validateName}>
22+
{{
23+
label: 'Upload a file named "validfile.txt"'
24+
}}
25+
</FileUploader>
26+
</Example>
27+
);
28+
});

src/examples/src/widgets/file-uploader/Validated.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Example from '../../Example';
55
const factory = create();
66

77
export default factory(function Validated() {
8-
const accept = ['image/jpeg', 'image/png'];
8+
const accept = 'image/jpeg,image/png';
99
const maxSize = 50000;
1010

1111
return (

src/file-upload-input/design.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ dialog but you have to call its `click` method.
66

77
The overlay `<div>` provides a visual indicator that a drag operation is in progress.
88

9+
The widget accepts children so that widgets using FileUploadInput can render file information within the bounds of
10+
the FileUploadInput and when the overlay is displayed it will cover the children as well.
11+
912
# Drag and Drop
1013

1114
https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
@@ -28,3 +31,8 @@ the cursor moves over children (even letters in text).
2831
- when this event fires the overlay is hidden
2932
- `drop`: listened for on the root since it bubbles from the overlay
3033
- get the files and update to indicate DnD is no longer active
34+
35+
# Validation
36+
37+
When the `accept` parameter is set on `<input type="file">` the dialog restricts which files can be selected. For
38+
Drag and Drop the validation has to be done by the widget, otherwise any file will be accepted.

src/file-upload-input/index.tsx

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export interface FileUploadInputChildren {
2020

2121
export interface FileUploadInputProperties {
2222
/** The `accept` attribute of the input */
23-
accept?: string | string[];
23+
accept?: string;
2424

2525
/** If `true` file drag-n-drop is allowed. Default is `true` */
2626
allowDnd?: boolean;
@@ -50,6 +50,43 @@ export interface FileUploadInputProperties {
5050
widgetId?: string;
5151
}
5252

53+
export function filterValidFiles(files: File[], accept: FileUploadInputProperties['accept']) {
54+
if (!accept) {
55+
return files;
56+
}
57+
58+
const { extensions, types } = accept.split(',').reduce(
59+
function(sum, acceptPattern) {
60+
if (acceptPattern.startsWith('.')) {
61+
sum.extensions.push(new RegExp(`\\${acceptPattern}$`, 'i'));
62+
} else {
63+
const wildcardIndex = acceptPattern.indexOf('/*');
64+
if (wildcardIndex > 0) {
65+
sum.types.push(
66+
new RegExp(`^${acceptPattern.substr(0, wildcardIndex)}/.+`, 'i')
67+
);
68+
} else {
69+
sum.types.push(new RegExp(acceptPattern, 'i'));
70+
}
71+
}
72+
73+
return sum;
74+
},
75+
{ extensions: [], types: [] } as { extensions: RegExp[]; types: RegExp[] }
76+
);
77+
78+
const validFiles = files.filter(function(file) {
79+
if (
80+
extensions.some((extensionRegex) => extensionRegex.test(file.name)) ||
81+
types.some((typeRegex) => typeRegex.test(file.type))
82+
) {
83+
return true;
84+
}
85+
});
86+
87+
return validFiles;
88+
}
89+
5390
export interface ValidationInfo {
5491
message?: string;
5592
valid?: boolean;
@@ -85,7 +122,7 @@ export const FileUploadInput = factory(function FileUploadInput({
85122
} = properties();
86123
const { messages } = i18n.localize(bundle);
87124
const themeCss = theme.classes(css);
88-
const { content, label } = children()[0] || {};
125+
const { content = null, label = null } = children()[0] || {};
89126
let isDndActive = icache.getOrSet('isDndActive', false);
90127

91128
function onDragEnter(event: DragEvent) {
@@ -107,7 +144,11 @@ export const FileUploadInput = factory(function FileUploadInput({
107144
icache.set('isDndActive', false);
108145

109146
if (onValue && event.dataTransfer && event.dataTransfer.files.length) {
110-
onValue(Array.from(event.dataTransfer.files));
147+
const fileArray = Array.from(event.dataTransfer.files);
148+
const validFiles = filterValidFiles(fileArray, accept);
149+
if (validFiles.length) {
150+
onValue(validFiles);
151+
}
111152
}
112153
}
113154

src/file-upload-input/tests/unit/FileUploadInput.spec.tsx

Lines changed: 82 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,66 @@ import { tsx } from '@dojo/framework/core/vdom';
22
import { assertion, renderer, wrap } from '@dojo/framework/testing/renderer';
33
import { Button } from '../../../button';
44
import { FileUploadInput } from '../../index';
5+
import { Label } from '../../../label';
56
import { noop } from '../../../common/tests/support/test-helpers';
67

78
import bundle from '../../nls/FileUploadInput';
89
import * as baseCss from '../../../theme/default/base.m.css';
910
import * as buttonCss from '../../../theme/default/button.m.css';
1011
import * as css from '../../../theme/default/file-upload-input.m.css';
1112
import * as fixedCss from '../../styles/file-upload-input.m.css';
13+
import * as labelCss from '../../../theme/default/label.m.css';
1214

1315
const { it, describe } = intern.getInterface('bdd');
1416
const { messages } = bundle;
1517

1618
describe('FileUploadInput', function() {
17-
const WrappedButton = wrap(Button);
18-
const WrappedDndLabel = wrap('span');
19-
const WrappedOverlay = wrap('div');
19+
const WrappedRoot = wrap('div');
20+
const WrappedLabel = wrap('span');
21+
const baseRootProperties = {
22+
key: 'root',
23+
classes: [null, fixedCss.root, css.root, false, false],
24+
ondragenter: noop,
25+
ondragover: noop,
26+
ondrop: noop
27+
};
2028

2129
const baseAssertion = assertion(function() {
2230
return (
23-
<div key="root" classes={[undefined, fixedCss.root, css.root, false, false]}>
24-
<input
25-
key="nativeInput"
26-
accept={undefined}
27-
aria="hidden"
28-
classes={[baseCss.hidden]}
29-
click={noop}
30-
disabled={false}
31-
multiple={false}
32-
name={undefined}
33-
onchange={noop}
34-
required={false}
35-
type="file"
36-
/>
37-
<WrappedButton
38-
disabled={false}
39-
onClick={noop}
40-
theme={{
41-
'@dojo/widgets/button': {
42-
disabled: buttonCss.disabled,
43-
label: buttonCss.label,
44-
popup: buttonCss.popup,
45-
pressed: buttonCss.pressed,
46-
root: buttonCss.root
47-
}
48-
}}
49-
>
50-
{messages.chooseFiles}
51-
</WrappedButton>
52-
<WrappedDndLabel classes={[css.dndLabel]}>
53-
{messages.orDropFilesHere}
54-
</WrappedDndLabel>
55-
<WrappedOverlay
56-
key="overlay"
57-
classes={[fixedCss.dndOverlay, css.dndOverlay, baseCss.hidden]}
58-
/>
59-
</div>
31+
<WrappedRoot {...baseRootProperties}>
32+
<div classes={[css.wrapper]}>
33+
<input
34+
key="nativeInput"
35+
accept={undefined}
36+
aria="hidden"
37+
classes={[baseCss.hidden]}
38+
click={noop}
39+
disabled={false}
40+
multiple={false}
41+
name={undefined}
42+
onchange={noop}
43+
required={false}
44+
type="file"
45+
/>
46+
<Button
47+
disabled={false}
48+
onClick={noop}
49+
theme={{
50+
'@dojo/widgets/button': {
51+
disabled: buttonCss.disabled,
52+
label: buttonCss.label,
53+
popup: buttonCss.popup,
54+
pressed: buttonCss.pressed,
55+
root: buttonCss.root
56+
}
57+
}}
58+
>
59+
{messages.chooseFiles}
60+
</Button>
61+
62+
<WrappedLabel classes={[css.dndLabel]}>{messages.orDropFilesHere}</WrappedLabel>
63+
</div>
64+
</WrappedRoot>
6065
);
6166
});
6267

@@ -68,25 +73,44 @@ describe('FileUploadInput', function() {
6873
r.expect(baseAssertion);
6974
});
7075

71-
it('renders labels', function() {
72-
const buttonLabel = 'Button label';
73-
const dndLabel = 'Dnd label';
76+
it('renders label', function() {
77+
const label = 'Widget label';
7478

7579
const r = renderer(function() {
7680
return (
7781
<FileUploadInput>
7882
{{
79-
buttonLabel,
80-
dndLabel
83+
label
8184
}}
8285
</FileUploadInput>
8386
);
8487
});
8588

8689
r.expect(
87-
baseAssertion
88-
.setChildren(WrappedButton, () => [buttonLabel])
89-
.setChildren(WrappedDndLabel, () => [dndLabel])
90+
baseAssertion.prepend(WrappedRoot, () => [
91+
<Label
92+
disabled={false}
93+
forId={'file-upload-input-test'}
94+
hidden={false}
95+
required={false}
96+
theme={{
97+
'@dojo/widgets/label': {
98+
active: labelCss.active,
99+
disabled: labelCss.disabled,
100+
focused: labelCss.focused,
101+
invalid: labelCss.invalid,
102+
readonly: labelCss.readonly,
103+
required: labelCss.required,
104+
root: labelCss.root,
105+
secondary: labelCss.secondary,
106+
valid: labelCss.valid
107+
}
108+
}}
109+
valid={true}
110+
>
111+
{label}
112+
</Label>
113+
])
90114
);
91115
});
92116

@@ -95,6 +119,15 @@ describe('FileUploadInput', function() {
95119
return <FileUploadInput allowDnd={false} />;
96120
});
97121

98-
r.expect(baseAssertion.remove(WrappedDndLabel).remove(WrappedOverlay));
122+
r.expect(
123+
baseAssertion
124+
.setProperties(WrappedRoot, {
125+
...baseRootProperties,
126+
ondragenter: false,
127+
ondragover: false,
128+
ondrop: false
129+
})
130+
.remove(WrappedLabel)
131+
);
99132
});
100133
});

0 commit comments

Comments
 (0)