Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File picker component #50

Closed
wants to merge 61 commits into from
Closed
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
a620ed7
Bump react as hooks weren't supported until 16.8 and we're generally …
roblevintennis Aug 9, 2019
3c67b3c
Initial scaffolding for file picker. It actually does accept files an…
roblevintennis Aug 9, 2019
41b7799
Merge github.com:mavenlink/design-system into file-picker-component
roblevintennis Aug 12, 2019
4d2c737
Remove needless import
roblevintennis Aug 12, 2019
2d60700
Fix the issue with file open dialog opening twice on a single click
roblevintennis Aug 12, 2019
8e8dd01
Single and multiple file select examples
roblevintennis Aug 12, 2019
049606a
Adds single and multi examples
roblevintennis Aug 12, 2019
d29d361
Cleanup
roblevintennis Aug 12, 2019
eed85ae
styleguidist 9.0.4 still works. Higher versions use React.Fragment <>…
roblevintennis Aug 12, 2019
e413e3a
Removeing label onClick which appears not to be semantic / accessible…
roblevintennis Aug 12, 2019
06eae27
Adds type sizes
roblevintennis Aug 12, 2019
83b1dda
Starting to spike on Nathan's design a bit
roblevintennis Aug 12, 2019
73bc6bf
Looks so much nicer once you get the dashed border on it
roblevintennis Aug 12, 2019
10dc2cb
Fix lint issues
roblevintennis Aug 13, 2019
e695c00
Remove the background and border now that we're using a <button> for …
roblevintennis Aug 13, 2019
787c38a
Deal with case where the user clicks the 'Cancel' button on the nati…
roblevintennis Aug 13, 2019
c4cbfe1
Upgrade react-test-renderer / enzyme to support Hooks. Adds @testing-…
roblevintennis Aug 13, 2019
9e3e4dc
Adds onBeforeFileRemoved prop. Adds initial spec.
roblevintennis Aug 13, 2019
ac3b071
API coverage
roblevintennis Aug 13, 2019
b03352f
Fix lint issues
roblevintennis Aug 13, 2019
aa8cd17
Adds some functional testing too
roblevintennis Aug 14, 2019
76641bd
Lint fixes
roblevintennis Aug 14, 2019
34842bd
Adds the upload and file icons
roblevintennis Aug 14, 2019
f6c4fa6
Adds the new neutral palette colors for fills and strokes on icons
roblevintennis Aug 14, 2019
e747109
Adds upload and file icons to the file picker. Fix extra line in icon…
roblevintennis Aug 14, 2019
2336b2e
Proper styling for the upload icon
roblevintennis Aug 14, 2019
c5a3b32
Corresponding stroke-none added to icon component
roblevintennis Aug 14, 2019
2bd7480
Adds SVGs to the Jest moduleNameMapper to prevent unrecognized file i…
roblevintennis Aug 14, 2019
8cece36
Implements receiveFilesChangedUpdates so that the user of the file pi…
roblevintennis Aug 14, 2019
69ac678
Code cleanup
roblevintennis Aug 14, 2019
fcabf67
Adds a nice example of how to use the receiveFilesChangedUpdates call…
roblevintennis Aug 14, 2019
0ff2cc4
Renamed the callback prop to receiveFilesChanged and improve the docs.
roblevintennis Aug 14, 2019
1d307ac
Snagging the accessible colors caution red
roblevintennis Aug 15, 2019
6c7ef0f
Adds a way to inject error messaging to the file picker. If you resel…
roblevintennis Aug 15, 2019
556655f
Spec for Error API
roblevintennis Aug 15, 2019
4ee18c1
rename error to errorMessage to prevent naming collision
roblevintennis Aug 15, 2019
503fee1
Extract out the error handling into its own useError hook.
roblevintennis Aug 15, 2019
4f002a3
console log the actual File API files which may be a bit more interes…
roblevintennis Aug 15, 2019
dc99309
Adds accessibility. The 'dropzone' area is now tabbable and you can a…
roblevintennis Aug 16, 2019
41e67a7
Some lint based tweaks
roblevintennis Aug 16, 2019
3d09374
jsx-a11y override...role button for file-picker's label allows it to …
roblevintennis Aug 16, 2019
ce995a6
And we have draggable drop-zone folks
roblevintennis Aug 16, 2019
c7a8d84
Update eslint
roblevintennis Aug 16, 2019
73073ce
Cleanup the file picker and fix the props.dropzoneClasses failures
roblevintennis Aug 16, 2019
f630d40
Refactors the error handling. useError now will take a validator func…
roblevintennis Aug 19, 2019
e39ac94
Indicate in the documentation that you can either use the file picker…
roblevintennis Aug 19, 2019
368d50a
Prune todo comment
roblevintennis Aug 19, 2019
89d8a32
Italics are doing weird things in styleguidist
roblevintennis Aug 19, 2019
d6a9b18
Cross browser solution (fixes Firefox issue where the file dialog doe…
roblevintennis Aug 19, 2019
68ee789
Favor using an override directive from top-level .eslintrc for each o…
roblevintennis Aug 20, 2019
e4404e6
Fixes issue with no highlight happening when you drag a file over the…
roblevintennis Aug 20, 2019
07b8b2f
Use an unordered list for the FileList since it's more semantic then …
roblevintennis Aug 20, 2019
5e5501b
Fix some lint issues
roblevintennis Aug 20, 2019
9bc41dd
Include file extensions on imports. Testing in the Visual Studio / cu…
roblevintennis Aug 21, 2019
eb7d938
Ensures we are always explicit about extensions to avoid overly confi…
juanca Aug 1, 2019
2fc31d2
Bumping
roblevintennis Aug 21, 2019
abe5880
Remove unused from cherry picking
roblevintennis Aug 21, 2019
87d3b49
Fixes another import without extension
roblevintennis Aug 21, 2019
9a80bd8
Fix typo for svgo
roblevintennis Aug 21, 2019
69af234
Fixes an issue with styleguidist per their issue tracker recommendations
roblevintennis Aug 21, 2019
8786ab7
Fix last of lint errors
roblevintennis Aug 21, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"homepage": "https://mavenlink.github.io/design-system",
"jest": {
"moduleNameMapper": {
"\\.css$": "identity-obj-proxy"
"\\.(css|svg)$": "identity-obj-proxy"
},
"setupTestFrameworkScriptFile": "<rootDir>/setup-enzyme.js",
"testPathIgnorePatterns": [
Expand All @@ -48,35 +48,37 @@
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.2.3",
"@babel/preset-react": "^7.0.0",
"@testing-library/react": "^9.1.1",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kent Dodds popular testing framework:
https://testing-library.com/docs/react-testing-library/intro

We're using this in bigmaven already and it's got some things that make it easier to test uncontrolled components too.

"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.5",
"css-loader": "^2.1.0",
"cypress": "^3.1.4",
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "^1.7.1",
"eslint": "^4.3.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"eslint": "^6.1.0",
"eslint-config-mavenlint-react": "^2.0.0",
"eslint-plugin-cypress": "^2.2.0",
"file-loader": "^3.0.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^23.6.0",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.5.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react": "^16.9.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Hooks support started in 16.8 and also we might as well keep up (bigmaven is already using hooks for example!)

"react-dom": "^16.9.0",
"react-ga": "^2.5.6",
"react-styleguidist": "^8.0.6",
"react-styleguidist": "9.0.4",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, we cannot upgrade more then this for the time being because we do not yet support the React.Fragment shorthand and they started using this in a couple of places 😛

"react-test-renderer": "^16.9.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upgraded some of these libraries to be compatible with React Hooks:
enzymejs/enzyme#1938

"react-transition-group": "^2.5.3",
"style-loader": "^0.23.1",
"svg-sprite-loader": "^3.8.0",
"svgo": "^1.3,0",
"svgo-loader": "^2.1.0",
"stylelint": "^10.1.0",
"stylelint-config-css-modules": "^1.4.0",
"stylelint-config-standard": "^18.3.0",
"stylelint-css-modules": "^0.9.0",
"svg-sprite-loader": "^3.8.0",
"svgo": "^1.3,0",
"svgo-loader": "^2.1.0",
"url-loader": "^1.1.2",
"wait-on": "^3.2.0",
"webpack": "^4.28.3"
Expand Down
14 changes: 14 additions & 0 deletions src/components/file-picker/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can define the overrides in the top-level eslintrc with a files regex.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 68ee789

"rules": {
// Override: role button for file-picker's label allows it to
// behave as a file dialog open button in my VoiceOver testing
"jsx-a11y/no-noninteractive-element-to-interactive-role": [
'error',
{
label: [
'button',
],
},
]
}
}
110 changes: 110 additions & 0 deletions src/components/file-picker/file-picker.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
@import "../../styles/colors.css";
@import "../../styles/spacing.css";
@import "../../styles/typography.css";

.title,
.label,
.filename,
.remove {
font-family: var(--mavenlink-type-font-family);
}

.dropzone {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
text-align: center;
background: var(--palette-neutral-x-light);
min-height: 100px;
vertical-align: middle;
border: 2px dashed var(--palette-grey-light);
}

.highlight-dropzone {
border: 2px solid var(--palette-grey-base);
opacity: 0.4;
}

.label-base {
display: block;
flex: 1;
margin: 0 0 var(--spacing-small);
color: var(--palette-grey-dark);
cursor: pointer;
}

.label {
composes: label-base;
line-height: 100px;
}

.filename,
.label {
font-size: var(--mavenlink-type-base-size);
}

.title {
composes: label-base;
font-size: var(--mavenlink-type-small-size);
}

.file {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
border: 0;
padding: 0;
clip: rect(0 0 0 0);
overflow: hidden;
}

.label:focus {
outline: 1px dotted var(--palette-grey-dark);
outline: -webkit-focus-ring-color auto 5px;
}

.file-list {
display: flex;
flex-direction: column;
}

.file-list-button {
display: flex;
flex-direction: row;
border-bottom: 1px solid var(--palette-grey-light);
line-height: var(--spacing-x-large);
}

.icon {
align-self: center;
margin-right: var(--spacing-medium);
}

.upload {
color: var(--palette-grey-dark);
vertical-align: middle;
}

.upload-icon {
margin-top: -1px;
margin-right: var(--spacing-small);
}

.filename {
flex-grow: 1;
text-align: left;
color: var(--palette-grey-x-dark);
}

.remove {
flex: 0 0 50px;
cursor: pointer;
background: transparent;
border: none;
font-weight: bold;
color: var(--palette-grey-base);
text-align: center;
font-size: 18px;
}
162 changes: 162 additions & 0 deletions src/components/file-picker/file-picker.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import PropTypes from 'prop-types';
import React, { useState, useRef } from 'react';
import Icon from '../icon/icon';
import useError from '../../hooks/useError';
import styles from './file-picker.css';
import iconUpload from '../../svgs/icon-cloud-upload-negative.svg';
import iconFileDefault from '../../svgs/icon-file-default.svg';

const FilePicker = (props) => {
const {
dropzoneClasses,
labelClasses,
fileClasses,
fileListClasses,
id,
title,
receiveFilesChanged,
validator,
...rest
} = props;

const [files, setFiles] = useState([]);
const [getError, setError, validate] = useError(validator);
const [highlight, setHighlight] = useState(false);
const inputFile = useRef(null);

const setFilesChanged = (selectedFiles) => {
// Accounts for case where User's clicked 'Cancel' on native file dialog
if (selectedFiles.length) {
const currentFiles = [];
Array.from(selectedFiles).map(file => currentFiles.push(file));
const errMsg = validate(currentFiles);
if (errMsg.length) {
setError(errMsg);
} else {
setError('');
setFiles(currentFiles);
if (props.receiveFilesChanged) {
props.receiveFilesChanged.call(this, currentFiles);
}
}
}
};

const onFilesChanged = (e) => {
const selectedFiles = e.currentTarget.files;
setFilesChanged(selectedFiles);
};

const onRemoveFile = (e, file) => {
e.preventDefault();
const currentFiles = files.filter(f => f.name !== file.name);
setFiles(currentFiles);
if (props.receiveFilesChanged) {
props.receiveFilesChanged.call(this, currentFiles);
}
};

const onLabelKeypress = (e) => {
e.preventDefault();
if (e.key === 'Enter') {
inputFile.current.click();
}
};

// This prevents the dropEffect going from "copy" to "none"
// (in chrome, cursor turns from a green plus to an arrow)
// https://github.com/react-dropzone/react-dropzone/blob/master/src/index.js#L533
const fixDropEffect = (e) => {
e.stopPropagation();
if (e.dataTransfer) {
try {
e.dataTransfer.dropEffect = 'copy';
} catch {} /* eslint-disable-line no-empty */
}
};

const onDragOver = (e) => {
e.preventDefault();
fixDropEffect(e);
};

const onDragLeave = () => {
setHighlight(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing setHighlight(true) in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e4404e6

};

const onDrop = (e) => {
e.preventDefault();
e.stopPropagation();
const droppedFiles = e.dataTransfer.files;
setFilesChanged(droppedFiles);
setHighlight(false);
};

const getFilesList = () => {
if (files.length) {
return files.map((file) => {
return (
<section className={styles['file-list-button']} key={file.name}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's convert this list of <section>s into a list of <li>s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in: 07b8b2f

<Icon className={styles.icon} name={iconFileDefault.id} size="medium" stroke="grey-base" fill="none" title="Upload file icon" />
<span className={styles.filename}>{file.name}</span>
<button onClick={e => onRemoveFile(e, file)} className={styles.remove}>&times;</button>
</section>
);
});
}
return '';
};

const getDropzoneClasses = () => {
const highlightClasses = highlight ? ` ${styles['highlight-dropzone']}` : '';
return `${props.dropzoneClasses}${highlightClasses}`;
};

return (
<React.Fragment>
<span className={styles.title}>{props.title}</span>{getError()}
<section className={props.fileListClasses}>
{getFilesList()}
</section>
<section
className={getDropzoneClasses()}
draggable
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
>
<label onKeyDown={onLabelKeypress} htmlFor={props.id} className={props.labelClasses} tabIndex="0" role="button" aria-controls={props.id}>
<Icon className={styles['upload-icon']} name={iconUpload.id} size="medium" fill="grey-dark" stroke="none" title="Upload file icon" />
<span className={styles.upload}>Upload Files</span>
<input onChange={e => onFilesChanged(e)} type="file" id={props.id} className={props.fileClasses} ref={inputFile} {...rest} />
</label>
</section>
</React.Fragment>
);
};

FilePicker.propTypes = {
dropzoneClasses: PropTypes.string,
fileClasses: PropTypes.string,
fileListClasses: PropTypes.string,
id: PropTypes.string.isRequired,
labelClasses: PropTypes.string,
title: PropTypes.string.isRequired,
multiple: PropTypes.string,
receiveFilesChanged: PropTypes.func,
validator: PropTypes.func,
};

FilePicker.defaultProps = {
dropzoneClasses: styles.dropzone,
fileClasses: styles.file,
fileListClasses: styles['file-list'],
id: undefined,
labelClasses: styles.label,
title: undefined,
multiple: undefined,
receiveFilesChanged: undefined,
validator: undefined,
};

export default FilePicker;
49 changes: 49 additions & 0 deletions src/components/file-picker/file-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Default usage

You can either select files via the file dialog picker, or, drop
the files onto the "dropzone".

```jsx
<FilePicker id="le-picker-single" title="Attach Files" />
```

Multiple

```jsx
<FilePicker id="le-picker-multi" title="Attach Multiple Files" multiple='multiple' />
```

The `receiveFilesChanged` prop allows you to receive notifications when
the _FileList_ has changed. (see
[File API](https://developer.mozilla.org/en-US/docs/Web/API/File_and_Directory_Entries_API))
You can then iteract with these files using the [File API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL)
per file. For example, you may wish to do something like
`FileReader.readAsDataURL(someFile)` before uploading to a server endpoint.

Interact with the following example component and view the console output.

```jsx
const callback = filelist => { filelist.forEach(file => console.log('file: ', file)) }
<FilePicker receiveFilesChanged={callback} id="le-picker-notify" title="Attach" multiple='multiple' />
```

Errors

Pass a `validator` function that verifies each of the files and returns an
error message. The component will then take care of displaying these returned
errors properly. To see this, pick a file greater then 10MB:

```jsx
const fileSizeError = 'The file size cannot exceed 10MB.';
const validateFileSize = (files) => {
const max = 10240000;
const errors = files.reduce((acc, file) => {
if (file.size > max) {
acc.push(`${file.name} too big. ${fileSizeError}`);
}
return acc;
}, []);
return errors.join(' ');
}
<FilePicker validator={validateFileSize} id="validator-example" title="Attach Files" />
```
Loading