Skip to content

Commit

Permalink
Add file-picker component (#109)
Browse files Browse the repository at this point in the history
* Add file-picker component

* Remove unused files

* Add disabled styles and use t.true()

* Tweak disabled style
  • Loading branch information
Rowno authored Jan 18, 2018
1 parent cc08cf7 commit 52f981a
Show file tree
Hide file tree
Showing 15 changed files with 536 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"lint-staged": "^6.0.0",
"prettier": "^1.9.1",
"react-test-renderer": "^16.2.0",
"sinon": "^4.1.6",
"url-loader": "^0.6.2",
"xo": "^0.18.2"
}
Expand Down
18 changes: 9 additions & 9 deletions packages/evergreen-alert/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,53 @@ import { shallow } from 'enzyme'
import { CheckCircleIcon } from 'evergreen-icons'
import { Alert } from '../src'

test('alert: basic snapshot', t => {
test('basic snapshot', t => {
const component = <Alert title="A simple general message" />
const tree = render.create(component).toJSON()
t.snapshot(tree)
})

test('alert: outputs title', t => {
test('outputs title', t => {
const component = shallow(<Alert title="Test title" />)
t.true(component.contains('Test title'))
})

test('alert: outputs children', t => {
test('outputs children', t => {
const component = shallow(<Alert title="Test title">Test content</Alert>)
t.true(component.contains('Test content'))
})

test('alert: type snapshot', t => {
test('type snapshot', t => {
const component = <Alert title="Test title" type="danger" />
const tree = render.create(component).toJSON()
t.snapshot(tree)
})

test('alert: hasTrim=true snapshot', t => {
test('hasTrim=true snapshot', t => {
const component = <Alert title="Test title" hasTrim />
const tree = render.create(component).toJSON()
t.snapshot(tree)
})

test('alert: hasTrim=false snapshot', t => {
test('hasTrim=false snapshot', t => {
const component = <Alert title="Test title" hasTrim={false} />
const tree = render.create(component).toJSON()
t.snapshot(tree)
})

test('alert: outputs icon (hasIcon=true)', t => {
test('outputs icon (hasIcon=true)', t => {
const component = shallow(<Alert title="Test title" type="success" hasIcon />)
t.true(component.containsMatchingElement(<CheckCircleIcon />))
})

test('alert: does not output icon (hasIcon=false)', t => {
test('does not output icon (hasIcon=false)', t => {
const component = shallow(
<Alert title="Test title" type="success" hasIcon={false} />
)
t.false(component.containsMatchingElement(<CheckCircleIcon />))
})

test('alert: appearance snapshot', t => {
test('appearance snapshot', t => {
const component = <Alert title="Test title" appearance="card" />
const tree = render.create(component).toJSON()
t.snapshot(tree)
Expand Down
10 changes: 5 additions & 5 deletions packages/evergreen-alert/test/snapshots/index.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The actual snapshot is saved in `index.js.snap`.

Generated by [AVA](https://ava.li).

## alert: appearance snapshot
## appearance snapshot

> Snapshot 1
Expand All @@ -23,7 +23,7 @@ Generated by [AVA](https://ava.li).
</div>
</div>

## alert: basic snapshot
## basic snapshot

> Snapshot 1
Expand All @@ -42,7 +42,7 @@ Generated by [AVA](https://ava.li).
</div>
</div>

## alert: hasTrim=false snapshot
## hasTrim=false snapshot

> Snapshot 1
Expand All @@ -61,7 +61,7 @@ Generated by [AVA](https://ava.li).
</div>
</div>

## alert: hasTrim=true snapshot
## hasTrim=true snapshot

> Snapshot 1
Expand All @@ -80,7 +80,7 @@ Generated by [AVA](https://ava.li).
</div>
</div>

## alert: type snapshot
## type snapshot

> Snapshot 1
Expand Down
Binary file modified packages/evergreen-alert/test/snapshots/index.js.snap
Binary file not shown.
1 change: 1 addition & 0 deletions packages/evergreen-buttons/src/components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export default class Button extends PureComponent {
paddingBottom={paddingBottom}
paddingRight={pr}
paddingLeft={pl}
margin={0} // Removes weird margins in Safari
{...textStyle}
css={{
...css,
Expand Down
19 changes: 19 additions & 0 deletions packages/evergreen-file-picker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "evergreen-file-picker",
"version": "1.0.0",
"description": "React components: FilePicker",
"main": "lib/index.js",
"keywords": ["evergreen", "segment", "ui", "react", "FilePicker"],
"author": "Segment",
"license": "MIT",
"dependencies": {
"evergreen-buttons": "^2.19.6",
"evergreen-text-input": "^2.19.6",
"prop-types": "^15.0.0",
"ui-box": "^0.5.4"
},
"peerDependencies": {
"react": "^16.0.0"
},
"xo": false
}
127 changes: 127 additions & 0 deletions packages/evergreen-file-picker/src/components/FilePicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Box from 'ui-box'
import { Button } from 'evergreen-buttons'
import { TextInput } from 'evergreen-text-input'

export const CLASS_PREFIX = 'evergreen-file-picker'

export default class FilePicker extends PureComponent {
static propTypes = {
name: PropTypes.string,
accept: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
]),
required: PropTypes.bool,
multiple: PropTypes.bool,
disabled: PropTypes.bool,
capture: PropTypes.bool,
height: PropTypes.number,
onChange: PropTypes.func
}

constructor() {
super()

this.state = {
files: []
}
}

render() {
const {
name,
accept,
required,
multiple,
disabled,
capture,
height,
...props
} = this.props
const { files } = this.state

let inputValue
if (files.length === 0) {
inputValue = ''
} else if (files.length === 1) {
inputValue = files[0].name
} else {
inputValue = `${files.length} files`
}

let buttonText
if (files.length === 0) {
buttonText = 'Select file'
} else if (files.length === 1) {
buttonText = 'Replace file'
} else {
buttonText = 'Replace files'
}

return (
<Box display="flex" className={`${CLASS_PREFIX}-root`} {...props}>
<Box
innerRef={this.fileInputRef}
className={`${CLASS_PREFIX}-file-input`}
is="input"
type="file"
name={name}
accept={accept}
required={required}
multiple={multiple}
disabled={disabled}
capture={capture}
onChange={this.handleFileChange}
display="none"
/>

<TextInput
className={`${CLASS_PREFIX}-text-input`}
readOnly
value={inputValue}
placeholder="Select a file to upload…"
// There's a weird specifity issue when there's two differently sized inputs on the page
borderTopRightRadius="0 !important"
borderBottomRightRadius="0 !important"
height={height}
flex={1}
textOverflow="ellipsis"
/>

<Button
className={`${CLASS_PREFIX}-button`}
onClick={this.handleButtonClick}
disabled={disabled}
borderTopLeftRadius={0}
borderBottomLeftRadius={0}
height={height}
flexShrink={0}
>
{buttonText}
</Button>
</Box>
)
}

fileInputRef = node => {
this.fileInput = node
}

handleFileChange = e => {
const { onChange } = this.props
const files = e.target.files

// Firefox returns the same array instance each time for some reason
this.setState({ files: [...files] })

if (onChange) {
onChange(files)
}
}

handleButtonClick = () => {
this.fileInput.click()
}
}
4 changes: 4 additions & 0 deletions packages/evergreen-file-picker/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import FilePicker, { CLASS_PREFIX } from './components/FilePicker'

export default FilePicker
export { FilePicker, CLASS_PREFIX }
19 changes: 19 additions & 0 deletions packages/evergreen-file-picker/stories/index.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { storiesOf } from '@storybook/react' // eslint-disable-line import/no-extraneous-dependencies
import React from 'react'
import Box from 'ui-box'
import { FilePicker } from '../src/'

storiesOf('file-picker', module).add('FilePicker', () => (
<Box padding={40}>
{(() => {
document.body.style.margin = '0'
document.body.style.height = '100vh'
})()}

<FilePicker multiple width={250} marginBottom={32} />

<FilePicker multiple width={350} height={24} marginBottom={32} />

<FilePicker disabled width={250} marginBottom={32} />
</Box>
))
96 changes: 96 additions & 0 deletions packages/evergreen-file-picker/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* eslint-disable import/no-extraneous-dependencies */
import React from 'react'
import test from 'ava'
import render from 'react-test-renderer'
import { shallow } from 'enzyme'
import { FilePicker, CLASS_PREFIX } from '../src'

test('snapshot', t => {
const component = <FilePicker />
const tree = render.create(component).toJSON()
t.snapshot(tree)
})

test('sets name', t => {
const component = shallow(<FilePicker name="hi" />)
t.is(component.find(`.${CLASS_PREFIX}-file-input`).prop('name'), 'hi')
})

test('sets accept', t => {
const component = shallow(<FilePicker accept="application/json" />)
t.is(
component.find(`.${CLASS_PREFIX}-file-input`).prop('accept'),
'application/json'
)
})

test('sets required', t => {
const component = shallow(<FilePicker required />)
t.true(component.find(`.${CLASS_PREFIX}-file-input`).prop('required'))
})

test('sets multiple', t => {
const component = shallow(<FilePicker multiple />)
t.true(component.find(`.${CLASS_PREFIX}-file-input`).prop('multiple'))
})

test('sets disabled', t => {
const component = shallow(<FilePicker disabled />)
t.true(component.find(`.${CLASS_PREFIX}-file-input`).prop('disabled'))
t.true(component.find(`.${CLASS_PREFIX}-button`).prop('disabled'))
})

test('sets capture', t => {
const component = shallow(<FilePicker capture />)
t.true(component.find(`.${CLASS_PREFIX}-file-input`).prop('capture'))
})

test('passes through height', t => {
const component = shallow(<FilePicker height={20} />)
t.is(component.find(`.${CLASS_PREFIX}-text-input`).prop('height'), 20)
t.is(component.find(`.${CLASS_PREFIX}-button`).prop('height'), 20)
})

test('passes through props', t => {
const component = shallow(<FilePicker width={20} />)
t.is(component.find(`.${CLASS_PREFIX}-root`).prop('width'), 20)
})

test('handles 1 file selected', t => {
const component = shallow(<FilePicker />)
const e = {
target: {
files: [{ name: 'data.json' }]
}
}
component.find(`.${CLASS_PREFIX}-file-input`).simulate('change', e)
t.deepEqual(component.state('files'), e.target.files)
t.is(component.find(`.${CLASS_PREFIX}-text-input`).prop('value'), 'data.json')
t.true(component.find(`.${CLASS_PREFIX}-button`).contains('Replace file'))
})

test('handles 2 files selected', t => {
const component = shallow(<FilePicker />)
const e = {
target: {
files: [{ name: 'data1.json' }, { name: 'data2.json' }]
}
}
component.find(`.${CLASS_PREFIX}-file-input`).simulate('change', e)
t.deepEqual(component.state('files'), e.target.files)
t.is(component.find(`.${CLASS_PREFIX}-text-input`).prop('value'), '2 files')
t.true(component.find(`.${CLASS_PREFIX}-button`).contains('Replace files'))
})

// Firefox returns the same array instance in each change event for some reason
test('clones files array', t => {
const component = shallow(<FilePicker />)
const e = {
target: {
files: [{ name: 'data.json' }]
}
}
component.find(`.${CLASS_PREFIX}-file-input`).simulate('change', e)
t.deepEqual(component.state('files'), e.target.files)
t.not(component.state('files'), e.target.files)
})
Loading

0 comments on commit 52f981a

Please sign in to comment.