Skip to content

Commit

Permalink
feat: add InputColor component
Browse files Browse the repository at this point in the history
  • Loading branch information
Federico Zivolo committed Jan 7, 2019
1 parent 51ff519 commit ca924ba
Show file tree
Hide file tree
Showing 6 changed files with 370 additions and 1 deletion.
22 changes: 22 additions & 0 deletions packages/react-forms/src/InputColor/InputColor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The `InputColor` component shares the same API of the HTML `input[type=color]` element.

```js
<InputColor onChange={e => console.log(e.target.value)} />
```

It also supports all the `InputText` and `Button` properties:

```js
<InputColor size="small" defaultValue="#FFF000" importance="primary" />
```

The component supports both uncontrolled and controlled API styles:

```js
initialState = { value: '#00FF00' };
<InputColor
size="large"
value={state.value}
onChange={evt => setState({ value: evt.target.value })}
/>;
```
135 changes: 135 additions & 0 deletions packages/react-forms/src/InputColor/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders the expected markup 1`] = `
.emotion-7 {
all: unset;
display: inline-block;
border-radius: 2px;
-webkit-transition: 0.2s ease-in-out border-color;
transition: 0.2s ease-in-out border-color;
border: 1px solid #8F9BA3;
}
.emotion-7:focus-within {
border-color: #00c1bb;
}
.emotion-7[data-invalid='true'] {
border-color: #E61E27;
}
.emotion-4 {
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
position: relative;
height: 32px;
}
.emotion-1 {
opacity: 0;
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
top: 0;
left: 0;
bottom: 0;
right: 0;
cursor: pointer;
}
.emotion-1:focus:not(:focus-visible) ~ button {
box-shadow: none;
}
.emotion-0 {
border-radius: 1px 0 0 1px;
border-left: 0;
border-top: 0;
border-bottom: 0;
}
.emotion-3 {
border-radius: 0 1px 1px 0;
border-right: 0;
border-top: 0;
border-bottom: 0;
}
<InputColor
onChange={[Function]}
>
<ForwardRef
className="emotion-9 emotion-6"
onChange={[Function]}
>
<InputGroup
className="emotion-9 emotion-6"
>
<fieldset
className="emotion-6 emotion-7 emotion-8"
data-invalid={false}
onChange={[Function]}
onInvalid={[Function]}
>
<input
className="emotion-0"
key=".0"
maxLength={7}
onChange={[Function]}
value="#000000"
/>
<Wrapper
key=".1"
>
<div
className="emotion-4 emotion-5"
>
<ColorSelector
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
type="color"
value="#000000"
>
<input
className="emotion-1 emotion-2"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
type="color"
value="#000000"
/>
</ColorSelector>
<button
className="emotion-3"
data-state=""
importance="secondary"
tabIndex={-1}
>
<x-icon
name="tint"
style={
Object {
"color": "#000000",
}
}
/>
</button>
</div>
</Wrapper>
</fieldset>
</InputGroup>
</ForwardRef>
</InputColor>
`;
128 changes: 128 additions & 0 deletions packages/react-forms/src/InputColor/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// @flow
import * as React from 'react';
import styled from '@emotion/styled/macro';
import callAll from '../utils/callAll';
import InputGroup from '../InputGroup';
import InputText, { HEIGHT } from '../InputText';
import Button from '../Button';
import { Icon } from '@quid/react-core';

type Props = {
onChange?: (SyntheticInputEvent<HTMLInputElement>) => void,
className?: string,
style?: Object,
innerRef?: React.ElementRef<any>,
disabled?: boolean,
defaultValue?: string,
size?: 'regular' | 'small' | 'large',
};

type State = {
color: string,
buttonState: Array<string>,
};

const addState = (...states) => ({ buttonState }) => ({
buttonState: [...new Set([...buttonState, ...states])],
});
const removeState = (...states) => ({ buttonState }) => ({
buttonState: buttonState.filter(s => !states.includes(s)),
});

const ColorSelector = styled.input`
opacity: 0;
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
top: 0;
left: 0;
bottom: 0;
right: 0;
cursor: pointer;
&:focus:not(:focus-visible) ~ ${Button} {
box-shadow: none;
}
`;

const Wrapper = styled.div`
display: inline-flex;
position: relative;
height: ${props => HEIGHT[props.size || 'regular']}px;
`;

class InnerInput extends React.Component<Props, State> {
state = {
color: this.props.defaultValue || '#000000',
buttonState: [],
};

static getDerivedStateFromProps(props, state) {
return { color: props.value || state.color };
}

input = React.createRef();

updateColor = ({ target: { value } }) =>
this.setState({ color: String(value).toUpperCase() });

render() {
const {
className,
style,
disabled,
innerRef,
onChange,
...props
} = this.props;
return (
<InputGroup style={style} className={className}>
{cn => (
<InputText
{...props}
className={cn}
onChange={callAll(this.updateColor, onChange)}
value={this.state.color}
maxLength={7}
disabled={disabled}
/>
)}
{cn => (
<Wrapper size={props.size}>
<ColorSelector
type="color"
value={this.state.color}
onChange={callAll(this.updateColor, onChange)}
onMouseDown={() => this.setState(addState('active'))}
onMouseUp={() => this.setState(removeState('active'))}
onMouseEnter={() => this.setState(addState('hover'))}
onMouseLeave={() => this.setState(removeState('hover', 'active'))}
onFocus={() => this.setState(addState('focus'))}
onBlur={() => this.setState(removeState('focus', 'active'))}
ref={innerRef}
disabled={disabled}
/>
<Button
importance="secondary"
{...props}
className={cn}
tabIndex={-1}
data-state={this.state.buttonState.join(' ')}
disabled={disabled}
>
<Icon name="tint" style={{ color: this.state.color }} />
</Button>
</Wrapper>
)}
</InputGroup>
);
}
}

const InputColor: React.StatelessFunctionalComponent<Props> = styled(
React.forwardRef((props, ref) => <InnerInput {...props} innerRef={ref} />)
)();

// @component
export default InputColor;
80 changes: 80 additions & 0 deletions packages/react-forms/src/InputColor/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// @flow
import React from 'react';
import ReactDOM from 'react-dom';
import { mount } from 'enzyme';
import InputColor from './index';

jest.mock('../InputText', () => ({
__esModule: true,
default: 'input',
HEIGHT: jest.requireActual('../InputText').HEIGHT,
}));
jest.mock('../Button', () => 'button');
jest.mock('@quid/react-core', () => ({
Icon: 'x-icon',
}));

const noop = () => undefined;

it('renders the expected markup', () => {
const wrapper = mount(<InputColor onChange={noop} />);

expect(wrapper).toMatchSnapshot();
});

it('responds to user interaction on fake button', () => {
const wrapper = mount(<InputColor onChange={noop} />);

wrapper.find('input[type="color"]').simulate('mouseEnter');
wrapper.find('input[type="color"]').simulate('mouseDown');
wrapper.find('input[type="color"]').simulate('focus');

expect(wrapper.find('button').prop('data-state')).toMatchInlineSnapshot(
`"hover active focus"`
);

wrapper.find('input[type="color"]').simulate('mouseLeave');
wrapper.find('input[type="color"]').simulate('mouseUp');
wrapper.find('input[type="color"]').simulate('blur');

expect(wrapper.find('button').prop('data-state')).toMatchInlineSnapshot(`""`);
});

it('renders a disabled component', () => {
const wrapper = mount(<InputColor disabled onChange={noop} />);

expect(
wrapper
.find('input')
.at(0)
.prop('disabled')
).toBe(true);
});

it('triggers onChange when color is changed using the `type=color` input', () => {
const handleChange = jest.fn();
const wrapper = mount(<InputColor onChange={handleChange} />);

wrapper
.find('input')
.at(1)
.simulate('change', { target: { value: '#000000' } });

expect(handleChange.mock.calls[0][0]).toMatchObject({
target: { value: '#000000' },
});
});

it('triggers onChange when color is changed using the `type=text` input', () => {
const handleChange = jest.fn();
const wrapper = mount(<InputColor onChange={handleChange} />);

wrapper
.find('input')
.at(0)
.simulate('change', { target: { value: '#000000' } });

expect(handleChange.mock.calls[0][0]).toMatchObject({
target: { value: '#000000' },
});
});
2 changes: 1 addition & 1 deletion packages/react-forms/src/InputText/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Props = {
// istanbul ignore next
const noop = () => undefined;

const HEIGHT = {
export const HEIGHT = {
large: 50,
small: 24,
regular: 32,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-forms/src/utils/inputPropsFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ export const INPUT_ATTRIBUTES = [
'list',
'min',
'max',
'minLength',
'maxLength',
'name',
'onChange',
'pattern',
'placeholder',
'readOnly',
'required',
'tabIndex',
'title',
'type',
'value',
];
Expand Down

0 comments on commit ca924ba

Please sign in to comment.