-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
A wrapper around the native file input that checks for extensions and file size. Returns a file object. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
A wrapper around the native file input that enforces extensions and file size, as well as specific dimensions. Returns a file object by default - or the base64 encoded string if specified otherwise. |
39 changes: 39 additions & 0 deletions
39
packages/quark-web/src/components/form/FilePicker/FileInput.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// @flow | ||
// external | ||
import * as React from 'react' | ||
import { View } from 'react-native-web' | ||
|
||
type Props = { | ||
children: React.Node, | ||
onChange: File => void, | ||
style: {} | ||
} | ||
|
||
class FileInput extends React.Component<Props> { | ||
fileInput: ?HTMLInputElement | ||
|
||
_handleUpload = (evt: Event) => { | ||
const file = evt.target.files[0] | ||
this.props.onChange(file) | ||
// free up the fileInput again | ||
this.fileInput.value = null | ||
} | ||
|
||
render() { | ||
return ( | ||
<View style={this.props.style}> | ||
<input | ||
type="file" | ||
style={{ display: 'none' }} | ||
onChange={this._handleUpload} | ||
ref={ele => (this.fileInput = ele)} | ||
/> | ||
{React.cloneElement(this.props.children, { | ||
onClick: () => this.fileInput.click() | ||
})} | ||
</View> | ||
) | ||
} | ||
} | ||
|
||
export default FileInput |
65 changes: 65 additions & 0 deletions
65
packages/quark-web/src/components/form/FilePicker/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
// @flow | ||
// external | ||
import * as React from 'react' | ||
// local | ||
import FileInput from './FileInput' | ||
|
||
type Props = { | ||
children: React.Node, | ||
onChange: File => void, | ||
onError: string => void, | ||
maxSize: number, | ||
extensions?: Array<string>, | ||
style?: {} | ||
} | ||
|
||
class FilePicker extends React.Component<Props> { | ||
static defaultProps = { | ||
maxSize: 2 | ||
} | ||
|
||
_validate = (file: File) => { | ||
const { onError, onChange, maxSize, extensions } = this.props | ||
|
||
// make sure a file was provided in the first place | ||
if (!file) { | ||
onError('Failed to upload a file.') | ||
return | ||
} | ||
|
||
// if we care about file extensions | ||
if (extensions) { | ||
const uploadedFileExt = file.name | ||
.split('.') | ||
.pop() | ||
.toLowerCase() | ||
const isValidFileExt = extensions | ||
.map(ext => ext.toLowerCase()) | ||
.includes(uploadedFileExt) | ||
|
||
if (!isValidFileExt) { | ||
onError(`Must upload a file of type: ${extensions.join(' or ')}`) | ||
return | ||
} | ||
} | ||
|
||
// convert maxSize from megabytes to bytes | ||
const maxBytes = maxSize * 1000000 | ||
|
||
if (file.size > maxBytes) { | ||
onError(`File size must be less than ${maxSize} MB.`) | ||
return | ||
} | ||
|
||
// return native file object | ||
onChange(file) | ||
} | ||
|
||
render = () => ( | ||
<FileInput onChange={this._validate} style={this.props.style}> | ||
{this.props.children} | ||
</FileInput> | ||
) | ||
} | ||
|
||
export default FilePicker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// external | ||
import React from 'react' | ||
import { mount } from 'enzyme' | ||
// local | ||
import FilePicker from '.' | ||
|
||
describe('File Picker', () => { | ||
let onChange | ||
let onError | ||
|
||
beforeEach(() => { | ||
onChange = jest.fn() | ||
onError = jest.fn() | ||
}) | ||
|
||
test('returns a valid component with required props', () => { | ||
const ele = ( | ||
<FilePicker onChange={() => ({})} onError={() => ({})}> | ||
<button>Click to upload</button> | ||
</FilePicker> | ||
) | ||
|
||
expect(React.isValidElement(ele)).toBe(true) | ||
}) | ||
|
||
test('call error handler when no file uploaded', () => { | ||
// mount the select with a few options | ||
const wrapper = mount( | ||
<FilePicker onChange={onChange} onError={onError}> | ||
<div>Click here</div> | ||
</FilePicker> | ||
) | ||
|
||
// trigger the onChange callback on file input | ||
wrapper.find('input').simulate('change', { target: { files: [] } }) | ||
|
||
expect(onError.mock.calls.length).toBe(1) | ||
expect(onChange.mock.calls.length).toBe(0) | ||
}) | ||
|
||
test('call error handler when a file with incorrect extension is uploaded', () => { | ||
// mount the select with a few options | ||
const wrapper = mount( | ||
<FilePicker onChange={onChange} onError={onError} extensions={['md']}> | ||
<div>Click here</div> | ||
</FilePicker> | ||
) | ||
|
||
const file = new Blob(['file contents'], { type: 'text/plain' }) | ||
file.name = 'file.txt' | ||
|
||
// trigger the onChange callback on file input | ||
wrapper.find('input').simulate('change', { target: { files: [file] } }) | ||
|
||
expect(onError.mock.calls.length).toBe(1) | ||
expect(onChange.mock.calls.length).toBe(0) | ||
}) | ||
|
||
test('call error handler when a file that is too large is uploaded', () => { | ||
// mount the select with a few options | ||
const wrapper = mount( | ||
<FilePicker | ||
onChange={onChange} | ||
onError={onError} | ||
// set unreasonably small max size so that our tiny blob is too big | ||
maxSize={0.0000000001} | ||
> | ||
<div>Click here</div> | ||
</FilePicker> | ||
) | ||
|
||
const file = new Blob(['file contents'], { type: 'text/plain' }) | ||
|
||
// trigger the onChange callback on file input | ||
wrapper.find('input').simulate('change', { target: { files: [file] } }) | ||
|
||
expect(onError.mock.calls.length).toBe(1) | ||
expect(onChange.mock.calls.length).toBe(0) | ||
}) | ||
|
||
test('call change handler when a file with correct size and extension is uploaded', () => { | ||
// mount the select with a few options | ||
const wrapper = mount( | ||
<FilePicker onChange={onChange} onError={onError} extensions={['txt']} maxSize={1}> | ||
<div>Click here</div> | ||
</FilePicker> | ||
) | ||
|
||
const file = new Blob(['file contents'], { type: 'text/plain' }) | ||
file.name = 'file.txt' | ||
|
||
// trigger the onChange callback on file input | ||
wrapper.find('input').simulate('change', { target: { files: [file] } }) | ||
|
||
expect(onError.mock.calls.length).toBe(0) | ||
expect(onChange.mock.calls.length).toBe(1) | ||
}) | ||
}) |
61 changes: 61 additions & 0 deletions
61
packages/quark-web/src/components/form/ImagePicker/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// @flow | ||
// external | ||
import * as React from 'react' | ||
// local | ||
import loadFile from './load-file' | ||
import loadImage from './load-image' | ||
import { FilePicker } from '..' | ||
|
||
type Props = { | ||
children: React.Node, | ||
onChange: File => File | string, | ||
onError: string => void, | ||
dims: { | ||
minWidth: number, | ||
maxWidth: number, | ||
minHeight: number, | ||
maxHeight: number | ||
}, | ||
base64?: boolean, | ||
maxSize?: number, | ||
extensions?: Array<string>, | ||
style?: {} | ||
} | ||
|
||
class UploadImage extends React.Component<Props> { | ||
static defaultProps = { | ||
base64: false | ||
} | ||
|
||
_handleImg = async (file: File) => { | ||
// grab used props | ||
const { onChange, onError, dims, base64 } = this.props | ||
|
||
try { | ||
const dataUrl = await loadFile(file) | ||
await loadImage(dataUrl, dims) | ||
if (base64) { | ||
return dataUrl | ||
} | ||
onChange(file) | ||
} catch (err) { | ||
// pass err message to onError handler | ||
onError(err.message) | ||
} | ||
} | ||
|
||
render() { | ||
const { children, ...unused } = this.props | ||
// pass our own onChange handler here and | ||
// use the user-provided onChange handler above in _handleImg | ||
Reflect.deleteProperty(unused, 'onChange') | ||
|
||
return ( | ||
<FilePicker onChange={this._handleImg} {...unused}> | ||
{children} | ||
</FilePicker> | ||
) | ||
} | ||
} | ||
|
||
export default UploadImage |
11 changes: 11 additions & 0 deletions
11
packages/quark-web/src/components/form/ImagePicker/load-file.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export default function loadFile(file) { | ||
return new Promise((resolve, reject) => { | ||
const reader = new FileReader() | ||
|
||
reader.readAsDataURL(file) | ||
|
||
reader.onloadend = loadedFile => resolve(loadedFile.target.result) | ||
|
||
reader.onerror = () => reject(new Error('There was an error uploading the file')) | ||
}) | ||
} |
35 changes: 35 additions & 0 deletions
35
packages/quark-web/src/components/form/ImagePicker/load-image.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
export default function loadImg(dataUrl, dims) { | ||
// destructure props from dims | ||
const { minWidth, maxWidth, minHeight, maxHeight } = dims | ||
|
||
return new Promise((resolve, reject) => { | ||
// create a new html image element | ||
const img = new Image() | ||
// set the image src attribute to our dataUrl | ||
img.src = dataUrl | ||
|
||
// listen for onload event | ||
img.onload = () => { | ||
// validate the min and max image dimensions | ||
if (img.width < minWidth || img.height < minHeight) { | ||
reject( | ||
new Error( | ||
`The uploaded image is too small. Must be at least ${minWidth}px by ${minHeight}px.` | ||
) | ||
) | ||
} | ||
|
||
if (img.width > maxWidth || img.height > maxHeight) { | ||
reject( | ||
new Error( | ||
`The uploaded image is too large. Must be no more than ${maxWidth}px by ${maxHeight}px.` | ||
) | ||
) | ||
} | ||
|
||
resolve(true) | ||
} | ||
|
||
img.onerror = () => reject(new Error('There was an error uploading the image')) | ||
}) | ||
} |
41 changes: 41 additions & 0 deletions
41
packages/quark-web/src/components/form/ImagePicker/test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// external | ||
import React from 'react' | ||
import { mount } from 'enzyme' | ||
// local | ||
import ImagePicker from '.' | ||
|
||
describe('Image File Input', () => { | ||
let onChange | ||
let onError | ||
const dims = { minWidth: 0, maxWidth: 10, minHeight: 0, maxHeight: 10 } | ||
|
||
beforeEach(() => { | ||
onChange = jest.fn() | ||
onError = jest.fn() | ||
}) | ||
|
||
test('returns a valid component with required props', () => { | ||
const ele = ( | ||
<ImagePicker onChange={() => ({})} onError={() => ({})} dims={dims}> | ||
<button>Click to upload</button> | ||
</ImagePicker> | ||
) | ||
|
||
expect(React.isValidElement(ele)).toBe(true) | ||
}) | ||
|
||
test('call the error handler when no image uploaded', () => { | ||
// mount the select with a few options | ||
const wrapper = mount( | ||
<ImagePicker onChange={onChange} onError={onError} dims={dims}> | ||
<div>Click here</div> | ||
</ImagePicker> | ||
) | ||
|
||
// trigger the onChange callback on file input | ||
wrapper.find('input').simulate('change', { target: { files: [] } }) | ||
|
||
expect(onError.mock.calls.length).toBe(1) | ||
expect(onChange.mock.calls.length).toBe(0) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters