Skip to content

Commit

Permalink
feat: add InputNumber component
Browse files Browse the repository at this point in the history
  • Loading branch information
Federico Zivolo committed Jan 2, 2019
1 parent d7bc0bb commit 353f254
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 37 deletions.
1 change: 1 addition & 0 deletions packages/react-forms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@quid/react-core": "^1.11.0",
"@quid/theme": "^1.11.0",
"color": "^3.1.0",
"react": "^16.7.0",
"react-router-dom": "^4.3.1"
},
"devDependencies": {
Expand Down
26 changes: 26 additions & 0 deletions packages/react-forms/src/InputNumber/InputNumber.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
The `InputNumber` component provides the same API of the HTML `<input type="number" />`,
with the addition of some useful features.

Default:

```js
<InputNumber min={-5} defaultValue={15} step={2} />
```

With optional unit (specified with the `unit` property):

```js
<InputNumber unit="pt" defaultValue={15} />
```

Disabled:

```js
<InputNumber defaultValue={15} disabled />
```

Read Only:

```js
<InputNumber defaultValue={15} readOnly />
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders the expected markup 1`] = `
<ContextConsumer>
<Component />
</ContextConsumer>
`;
78 changes: 78 additions & 0 deletions packages/react-forms/src/InputNumber/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// @flow
import * as React from 'react';
import styled from '@emotion/styled/macro';
import { withFallback as wf } from '@quid/theme';
import { Icon } from '@quid/react-core';
import InputText from '../InputText';

type Props = {
step?: number,
disabled?: boolean,
readOnly?: boolean,
unit?: string,
};

const Addon = styled.div`
display: flex;
flex-direction: column;
align-items: center;
align-self: stretch;
`;

const Unit = styled.div`
margin-left: -0.7em;
margin-right: 0.7em;
`;

const Caret = styled.button`
all: unset;
display: flex;
align-items: center;
&:focus-visible {
box-shadow: 0 0 0 0.5px ${wf(props => props.theme.background)},
0 0 2px 2px ${wf(props => props.theme.selected)};
}
&:not(:disabled):active {
color: ${wf(props => props.theme.selected)};
}
`;

const InputNumber = styled(
({ step = 1, disabled = false, readOnly = false, unit, ...props }: Props) => {
const input = React.createRef();
return (
<InputText
ref={input}
type="number"
step={step}
disabled={disabled}
readOnly={readOnly}
renderAddon={() => (
<>
<Unit>{unit}</Unit>
<Addon>
<Caret
disabled={disabled || readOnly}
onClick={() => input.current && input.current.stepUp(step)}
>
<Icon name="caret_up" />
</Caret>
<Caret
disabled={disabled || readOnly}
onClick={() => input.current && input.current.stepDown(step)}
>
<Icon name="caret_down" />
</Caret>
</Addon>
</>
)}
{...props}
/>
);
}
)`
text-align: right;
`;

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

it('renders the expected markup', () => {
const wrapper = shallow(<InputNumber />);
expect(wrapper).toMatchSnapshot();
});

it('increments on click on up arrow', () => {
const stepUp = jest.fn();
const wrapper = mount(<InputNumber defaultValue={0} />);
wrapper.find('input').getDOMNode().stepUp = stepUp;
wrapper
.find('button')
.at(0)
.simulate('click');

expect(stepUp).toBeCalled();
expect(stepUp.mock.calls.length).toBe(1);
});

it('decrements on click on down arrow', () => {
const stepDown = jest.fn();
const wrapper = mount(<InputNumber defaultValue={0} />);
wrapper.find('input').getDOMNode().stepDown = stepDown;
wrapper
.find('button')
.at(1)
.simulate('click');

expect(stepDown).toBeCalled();
expect(stepDown.mock.calls.length).toBe(1);
});
31 changes: 18 additions & 13 deletions packages/react-forms/src/InputText/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,31 @@ exports[`renders the expected markup 1`] = `
<InputText
data-foo="bar"
>
<Container
<ForwardRef
className="emotion-5 emotion-2"
data-foo="bar"
isInvalid={false}
>
<div
className="emotion-2 emotion-3 emotion-4"
<Container
className="emotion-5 emotion-2"
data-foo="bar"
isInvalid={false}
>
<Input
onChange={[Function]}
onInvalid={[Function]}
<div
className="emotion-2 emotion-3 emotion-4"
data-foo="bar"
>
<input
className="emotion-0 emotion-1"
<Input
onChange={[Function]}
onInvalid={[Function]}
/>
</Input>
</div>
</Container>
>
<input
className="emotion-0 emotion-1"
onChange={[Function]}
onInvalid={[Function]}
/>
</Input>
</div>
</Container>
</ForwardRef>
</InputText>
`;
59 changes: 36 additions & 23 deletions packages/react-forms/src/InputText/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ const include = obj => keys =>
return acc;
}, {});

// istanbul ignore next
const mergeRefs = (...refs: Array<any>) => (ref: any) => {
refs.forEach(resolvableRef => {
if (typeof resolvableRef === 'function') {
resolvableRef(ref);
} else if (resolvableRef != null) {
(resolvableRef: any).current = ref;
}
});
};

// istanbul ignore next
const noop = () => undefined;

Expand Down Expand Up @@ -128,29 +139,31 @@ const Container = styled.div`
`;

const InputText: React.StatelessFunctionalComponent<Props> = styled(
({ onChange, validationErrorMessage, ...props }: Props) => {
const input = React.createRef();
return (
<InvalidHandler errorMessage={validationErrorMessage}>
{(getInputProps, isInvalid) => (
<Container {...omit(props)(INPUT_ATTRIBUTES)} isInvalid={isInvalid}>
<Input
ref={input}
{...include(props)([...INPUT_ATTRIBUTES, 'disabled'])}
{...getInputProps({ onChange })}
/>
{props.renderAddon &&
props.renderAddon({
onClick: () =>
input.current
? input.current.focus()
: /* istanbul ignore next */ noop(),
})}
</Container>
)}
</InvalidHandler>
);
}
React.forwardRef(
({ onChange, validationErrorMessage, ...props }: Props, ref) => {
const input = React.createRef();
return (
<InvalidHandler errorMessage={validationErrorMessage}>
{(getInputProps, isInvalid) => (
<Container {...omit(props)(INPUT_ATTRIBUTES)} isInvalid={isInvalid}>
<Input
ref={mergeRefs(input, ref)}
{...include(props)([...INPUT_ATTRIBUTES, 'disabled'])}
{...getInputProps({ onChange })}
/>
{props.renderAddon &&
props.renderAddon({
onClick: () =>
input.current
? input.current.focus()
: /* istanbul ignore next */ noop(),
})}
</Container>
)}
</InvalidHandler>
);
}
)
)();

// @component
Expand Down
2 changes: 2 additions & 0 deletions packages/react-forms/src/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
// @flow
export { Button } from '@quid/react-core';
export { default as InputText } from './InputText';
export { default as InputNumber } from './InputNumber';
8 changes: 7 additions & 1 deletion packages/react-forms/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
import * as components from '.';

it('exports the expected number of components', () => {
expect(Object.keys(components)).toHaveLength(1);
expect(Object.keys(components)).toMatchInlineSnapshot(`
Array [
"Button",
"InputText",
"InputNumber",
]
`);
});

0 comments on commit 353f254

Please sign in to comment.