Skip to content

Commit b2cfe78

Browse files
authored
Merge pull request #116 from arturbien/feat/NumberInput
feat(numberfield): rewrite NumberField
2 parents c4f54a3 + 821d1a1 commit b2cfe78

File tree

5 files changed

+362
-177
lines changed

5 files changed

+362
-177
lines changed
Lines changed: 156 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,171 +1,198 @@
11
import React from 'react';
22
import propTypes from 'prop-types';
3-
43
import styled, { css } from 'styled-components';
54

5+
import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled';
6+
import { clamp } from '../common/utils';
7+
68
import Button from '../Button/Button';
79
import { blockSizes } from '../common/system';
810
import TextField from '../TextField/TextField';
911

10-
// ⭕⭕⭕⭕⭕ fix functionality and use hooks
11-
1212
const StyledNumberFieldWrapper = styled.div`
1313
display: inline-flex;
1414
align-items: center;
1515
`;
1616

17-
const StyledButtonWrapper = styled.div`
18-
height: ${blockSizes.md};
19-
display: flex;
20-
flex-direction: column;
21-
flex-wrap: nowrap;
22-
margin-left: 2px;
23-
margin-top: ${({ variant }) => (variant === 'default' ? '-2px' : '0')};
24-
`;
25-
2617
const StyledButton = styled(Button)`
27-
height: 50%;
2818
width: 30px;
2919
padding: 0;
3020
flex-shrink: 0;
3121
3222
${({ isFlat }) =>
33-
!isFlat &&
34-
css`
35-
&:before {
36-
border-left-color: ${({ theme }) => theme.borderLight};
37-
border-top-color: ${({ theme }) => theme.borderLight};
38-
box-shadow: inset 1px 1px 0px 1px ${({ theme }) => theme.borderLightest},
39-
inset -1px -1px 0 1px ${({ theme }) => theme.borderDark};
40-
}
41-
`}
23+
isFlat
24+
? css`
25+
height: calc(50% - 1px);
26+
`
27+
: css`
28+
height: 50%;
29+
&:before {
30+
border-left-color: ${({ theme }) => theme.borderLight};
31+
border-top-color: ${({ theme }) => theme.borderLight};
32+
box-shadow: inset 1px 1px 0px 1px
33+
${({ theme }) => theme.borderLightest},
34+
inset -1px -1px 0 1px ${({ theme }) => theme.borderDark};
35+
}
36+
`}
37+
`;
38+
39+
const StyledButtonWrapper = styled.div`
40+
display: flex;
41+
flex-direction: column;
42+
flex-wrap: nowrap;
43+
justify-content: space-between;
44+
45+
${({ isFlat }) =>
46+
isFlat
47+
? css`
48+
height: calc(${blockSizes.md} - 4px);
49+
`
50+
: css`
51+
height: ${blockSizes.md};
52+
margin-left: 2px;
53+
`}
4254
`;
4355

4456
const StyledButtonIcon = styled.span`
45-
position: absolute;
46-
left: 50%;
47-
top: 50%;
48-
transform: translate(-50%, -50%) ${props => props.invert && 'rotateZ(180deg)'};
4957
width: 0px;
5058
height: 0px;
51-
border-left: 4px solid transparent;
52-
border-right: 4px solid transparent;
5359
display: inline-block;
54-
border-top: 4px solid ${({ theme }) => theme.text};
55-
${StyledButton}:active & {
56-
margin-top: 2px;
60+
${({ invert }) =>
61+
invert
62+
? css`
63+
border-left: 4px solid transparent;
64+
border-right: 4px solid transparent;
65+
border-bottom: 4px solid ${({ theme }) => theme.text};
66+
`
67+
: css`
68+
border-left: 4px solid transparent;
69+
border-right: 4px solid transparent;
70+
border-top: 4px solid ${({ theme }) => theme.text};
71+
`}
72+
${StyledButton}:disabled & {
73+
filter: drop-shadow(1px 1px 0px ${({ theme }) => theme.textDisabledShadow});
74+
${({ invert }) =>
75+
invert
76+
? css`
77+
border-bottom-color: ${({ theme }) => theme.textDisabled};
78+
`
79+
: css`
80+
border-top-color: ${({ theme }) => theme.textDisabled};
81+
`}
5782
}
5883
`;
5984

60-
class NumberField extends React.Component {
61-
static propTypes = {
62-
variant: propTypes.oneOf(['default', 'flat']),
63-
onChange: propTypes.func.isRequired,
64-
value: propTypes.number.isRequired,
65-
min: propTypes.number,
66-
max: propTypes.number,
67-
width: propTypes.oneOfType([propTypes.string, propTypes.number]),
68-
disabled: propTypes.bool,
69-
disableKeyboardInput: propTypes.bool,
70-
className: propTypes.string,
71-
style: propTypes.shape([propTypes.string, propTypes.number])
72-
};
73-
74-
static defaultProps = {
75-
variant: 'default',
76-
disabled: false,
77-
min: null,
78-
max: null,
79-
width: null,
80-
disableKeyboardInput: false,
81-
className: '',
82-
style: {}
83-
};
84-
85-
state = {
86-
// eslint-disable-next-line
87-
value: parseInt(this.props.value, 10) || 0
85+
const NumberField = React.forwardRef(function NumberField(props, ref) {
86+
const {
87+
value,
88+
defaultValue,
89+
disabled,
90+
className,
91+
variant,
92+
step,
93+
width,
94+
min,
95+
max,
96+
onChange,
97+
style
98+
} = props;
99+
100+
const [valueDerived, setValueState] = useControlledOrUncontrolled({
101+
value,
102+
defaultValue
103+
});
104+
105+
const handleInputChange = e => {
106+
const newValue = e.target.value;
107+
setValueState(newValue);
88108
};
89109

90-
add = addValue => {
91-
const { value } = this.state;
92-
const { onChange } = this.props;
93-
94-
const newValue = this.normalize(value + addValue);
95-
onChange(newValue);
96-
this.setState({ value: newValue });
97-
};
110+
const handleClick = val => {
111+
const newValue = clamp(
112+
+parseFloat(valueDerived + val).toFixed(2),
113+
min,
114+
max
115+
);
98116

99-
handleChange = e => {
100-
let newValue =
101-
e.target.value === '-' ? '-' : this.normalize(e.target.value);
102-
// eslint-disable-next-line
103-
newValue = newValue ? newValue : newValue === 0 ? 0 : '';
117+
setValueState(newValue);
104118

105-
if (e.target.validity.valid) {
106-
const { onChange } = this.props;
107-
this.setState({ value: newValue });
119+
if (onChange) {
108120
onChange(newValue);
109121
}
110122
};
111123

112-
normalize = value => {
113-
const { min, max } = this.props;
114-
115-
if (min && value < min) return min;
116-
if (max && value > max) return max;
117-
118-
return parseInt(value, 10);
124+
const onBlur = () => {
125+
if (onChange) {
126+
onChange(valueDerived);
127+
}
119128
};
120129

121-
render() {
122-
const {
123-
disabled,
124-
disableKeyboardInput,
125-
className,
126-
variant,
127-
width,
128-
style
129-
} = this.props;
130-
const { value } = this.state;
131-
return (
132-
<StyledNumberFieldWrapper
133-
className={className}
134-
style={{ ...style, width: width || 'auto' }}
135-
>
136-
<TextField
137-
value={value}
130+
return (
131+
<StyledNumberFieldWrapper
132+
className={className}
133+
style={{ ...style, width: width || 'auto' }}
134+
>
135+
<TextField
136+
value={valueDerived}
137+
variant={variant}
138+
onChange={handleInputChange}
139+
disabled={disabled}
140+
type='number'
141+
ref={ref}
142+
width='100%'
143+
data-testid='input'
144+
onBlur={onBlur}
145+
/>
146+
<StyledButtonWrapper isFlat={variant === 'flat'}>
147+
<StyledButton
148+
data-testid='increment'
149+
isFlat={variant === 'flat'}
138150
variant={variant}
139-
onChange={
140-
disabled || disableKeyboardInput ? undefined : this.handleChange
141-
}
142-
readOnly={disabled || disableKeyboardInput}
143151
disabled={disabled}
144-
type='tel'
145-
pattern='^-?[0-9]\d*\.?\d*$'
146-
width='100%'
147-
/>
148-
<StyledButtonWrapper>
149-
<StyledButton
150-
isFlat={variant === 'flat'}
151-
variant={variant}
152-
disabled={disabled}
153-
onClick={() => this.add(1)}
154-
>
155-
<StyledButtonIcon invert />
156-
</StyledButton>
157-
<StyledButton
158-
isFlat={variant === 'flat'}
159-
variant={variant}
160-
disabled={disabled}
161-
onClick={() => this.add(-1)}
162-
>
163-
<StyledButtonIcon />
164-
</StyledButton>
165-
</StyledButtonWrapper>
166-
</StyledNumberFieldWrapper>
167-
);
168-
}
169-
}
152+
onClick={() => handleClick(step)}
153+
>
154+
<StyledButtonIcon invert />
155+
</StyledButton>
156+
<StyledButton
157+
data-testid='decrement'
158+
isFlat={variant === 'flat'}
159+
variant={variant}
160+
disabled={disabled}
161+
onClick={() => handleClick(-step)}
162+
>
163+
<StyledButtonIcon />
164+
</StyledButton>
165+
</StyledButtonWrapper>
166+
</StyledNumberFieldWrapper>
167+
);
168+
});
169+
170+
NumberField.defaultProps = {
171+
className: '',
172+
defaultValue: undefined,
173+
disabled: false,
174+
max: null,
175+
min: null,
176+
step: 1,
177+
onChange: null,
178+
style: {},
179+
value: undefined,
180+
variant: 'default',
181+
width: null
182+
};
183+
184+
NumberField.propTypes = {
185+
className: propTypes.string,
186+
defaultValue: propTypes.number,
187+
disabled: propTypes.bool,
188+
max: propTypes.number,
189+
min: propTypes.number,
190+
step: propTypes.number,
191+
onChange: propTypes.func,
192+
style: propTypes.shape([propTypes.string, propTypes.number]),
193+
value: propTypes.number,
194+
variant: propTypes.oneOf(['default', 'flat']),
195+
width: propTypes.oneOfType([propTypes.string, propTypes.number])
196+
};
170197

171198
export default NumberField;

0 commit comments

Comments
 (0)