Skip to content

Commit

Permalink
Replace react-color
Browse files Browse the repository at this point in the history
[finishes #160456513] and reduces dev JS blob by 200kb
  • Loading branch information
adamhooper committed Oct 11, 2018
1 parent 830ada2 commit 1ea848d
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 214 deletions.
23 changes: 23 additions & 0 deletions __mocks__/reactstrap/lib/Popover.js
@@ -0,0 +1,23 @@
import React from 'react'

export default class Popover extends React.PureComponent {
render () {
const { innerClassName, isOpen } = this.props
if (!isOpen) return null

const props = { ...this.props }
delete props.isOpen
delete props.innerClassName
delete props.toggle
delete props.target
delete props.boundariesElement

props.className = innerClassName || null

return (
<div className='mock-reactstrap-Popover'>
<div {...props}/>
</div>
)
}
}
60 changes: 25 additions & 35 deletions assets/css/components/Input_components.scss
Expand Up @@ -314,42 +314,32 @@ textarea[name="notes"], .editable-notes-read-only {
margin:auto;
}

.twitter-picker {
// Override react-color's style. We can't set `width` because react-color
// uses JS to set styles. But we can set `max-width`.
max-width: 24rem;

// Center horizontally
// (it's already position:relative)
left: -.6rem;

// Place vertically so triangle is just next to button
top: .3rem;

// Can't adjust 'border' or 'box-shadow'. TwitterPicker does, but far too
// pale. Instead, use '::before' and '::after' to create a triangle pointer
// ... can we all agree that reactcss is terrible for customization?
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid $light-gray;
.color-picker-popover {
button.color-choice {
height: 3rem;
width: 3rem;
cursor: pointer;
border-radius: .4rem;
margin: 0 .6rem .6rem 0;
border: none;
}

&::after {
content: '';
position: absolute;
top: -.5rem;
left: 1.1rem; // centered on button
width: 1rem;
height: 1rem;
background: white;
transform: rotate(-45deg);
border: 1px solid transparent;
border-right-color: $light-gray;
border-top-color: $light-gray;
button.choose-custom-color {
// These override Bootstrap's styles
transition: none; // animating color changes is so annoying
border: none;
width: 3rem;
}

input {
width: 6em;
font-size: 1.4rem;
height: 3rem;
border: 1px solid $light-gray;
border-radius: .4rem;

&:invalid {
border-color: $error-prompt;
}
}
}
163 changes: 163 additions & 0 deletions assets/js/ColorPicker.js
@@ -0,0 +1,163 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from 'reactstrap/lib/Button'
import InputGroup from 'reactstrap/lib/InputGroup'
import InputGroupAddon from 'reactstrap/lib/InputGroupAddon'
import Input from 'reactstrap/lib/Input'
import Popover from 'reactstrap/lib/Popover'
import PopoverBody from 'reactstrap/lib/PopoverBody'

class ColorChoice extends React.PureComponent {
static propTypes = {
color: PropTypes.string.isRequired, // like '#abcdef'
onClick: PropTypes.func.isRequired, // onClick('#abcdef') => undefined
}

onClick = () => {
this.props.onClick(this.props.color)
}

render () {
const { color } = this.props
const name = 'color-' + color.slice(1)

return (
<button
name={name}
onClick={this.onClick}
className='color-choice'
style={{ backgroundColor: color}}
></button>
)
}
}


/**
* a <button> and <input> that let the user write a color.
*/
class CustomColorChoice extends React.PureComponent {
static propTypes = {
defaultValue: PropTypes.string.isRequired, // '#abcdef'-style string
onChange: PropTypes.func.isRequired, // onChange('#abcdef') => undefined
onClose: PropTypes.func.isRequired, // onClose() => undefined -- if Escape is pressed
}

state = {
value: this.props.defaultValue,
}

get effectiveColor () {
if (!this.isValid) return '#000000'

const { value } = this.state
return value[0] === '#' ? value : `#${value}`
}

get isValid () {
return /^#?[0-9a-fA-F]{6}$/.test(this.state.value)
}

onClickButton = () => {
this.props.onChange(this.effectiveColor)
}

onChange = (ev) => {
this.setState({ value: ev.target.value })
}

onKeyDown = (ev) => {
switch (ev.key) {
case 'Enter':
const el = ev.target
if (this.isValid) {
this.props.onChange(this.effectiveColor)
return this.props.onClose()
}
break
case 'Escape':
this.props.onClose()
break
}
}

render () {
const { value } = this.state

return (
<InputGroup className={this.isValid ? 'valid' : 'invalid'}>
<InputGroupAddon addonType='prepend'>
<Button name='choose-custom-color' className='choose-custom-color' onClick={this.onClickButton} style={{ background: this.effectiveColor }}></Button>
</InputGroupAddon>
<Input
placeholder='#000000'
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
</InputGroup>
)
}
}


/**
* A simulation for `<input type="color" list=...`, which has lousy
* cross-browser support in 2018.
*/
export default class ColorPicker extends React.PureComponent {
static propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string, // Like '#abcdef'; default is '#000000'
choices: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
onChange: PropTypes.func.isRequired, // onChange('#abcdef') => undefined
}

state = {
isOpen: false
}

buttonRef = React.createRef()

toggleOpen = () => {
this.setState({ isOpen: !this.state.isOpen })
}

close = () => {
this.setState({ isOpen: false })
}

onChange = (color) => {
this.props.onChange(color)
this.setState({ isOpen: false })
}

render () {
const { name, value, choices } = this.props
const { isOpen } = this.state
const safeValue = value || '#000000'

return (
<div className='color-picker'>
<Button ref={this.buttonRef} title='Pick color' onClick={this.toggleOpen} className='color-picker button color' style={{ background: safeValue }}>
<i className='color-picker' />
</Button>
{ isOpen ? (
<Popover placement='bottom' innerClassName='color-picker-popover' isOpen={isOpen} target={this.buttonRef} toggle={this.close}>
<PopoverBody>
{choices.map(color => (
<ColorChoice key={'choice-' + color} color={color} onClick={this.onChange} />
))}
<CustomColorChoice
key={'custom-choice-' + safeValue}
defaultValue={safeValue}
onChange={this.onChange}
onClose={this.close}
/>
</PopoverBody>
</Popover>
) : null }
</div>
)
}
}
1 change: 1 addition & 0 deletions assets/js/__snapshots__/WfHamburgerMenu.test.js.snap
Expand Up @@ -59,6 +59,7 @@ exports[`WfHamburgerMenu renders logged in, non-read only 1`] = `
<button
aria-expanded={false}
aria-haspopup={true}
aria-label={null}
className="context-button btn btn-secondary"
onClick={[Function]}
title="menu"
Expand Down
1 change: 1 addition & 0 deletions assets/js/__snapshots__/WorkflowNavBar.test.js.snap
Expand Up @@ -326,6 +326,7 @@ exports[`WorkflowNavBar With user logged in, Duplicate button sends user to new
<button
aria-expanded={false}
aria-haspopup={true}
aria-label={null}
className="context-button btn btn-secondary"
onClick={[Function]}
title="menu"
Expand Down
5 changes: 5 additions & 0 deletions assets/js/__snapshots__/workflows.test.js.snap
Expand Up @@ -117,6 +117,7 @@ exports[`Workflow list page renders correctly 1`] = `
<button
aria-expanded={false}
aria-haspopup={true}
aria-label={null}
className="context-button btn btn-secondary"
onClick={[Function]}
title="menu"
Expand Down Expand Up @@ -339,6 +340,7 @@ exports[`Workflow list page renders correctly 1`] = `
<button
aria-expanded={false}
aria-haspopup={true}
aria-label={null}
className="context-button btn btn-secondary"
onClick={[Function]}
type="button"
Expand Down Expand Up @@ -530,6 +532,7 @@ exports[`Workflow list page renders correctly 1`] = `
<button
aria-expanded={false}
aria-haspopup={true}
aria-label={null}
className="context-button btn btn-secondary"
onClick={[Function]}
type="button"
Expand Down Expand Up @@ -721,6 +724,7 @@ exports[`Workflow list page renders correctly 1`] = `
<button
aria-expanded={false}
aria-haspopup={true}
aria-label={null}
className="context-button btn btn-secondary"
onClick={[Function]}
type="button"
Expand Down Expand Up @@ -912,6 +916,7 @@ exports[`Workflow list page renders correctly 1`] = `
<button
aria-expanded={false}
aria-haspopup={true}
aria-label={null}
className="context-button btn btn-secondary"
onClick={[Function]}
type="button"
Expand Down
Expand Up @@ -59,6 +59,7 @@ exports[`WfModuleContextMenu should match snapshot 1`] = `
<button
aria-expanded={false}
aria-haspopup={true}
aria-label={null}
className="context-button btn btn-secondary"
onClick={[Function]}
title="more"
Expand Down

0 comments on commit 1ea848d

Please sign in to comment.