Skip to content

Commit 4ad0374

Browse files
Merge pull request #702 from glints-dev/feature/next-input
Add Input
2 parents 2b337cb + e4303c2 commit 4ad0374

33 files changed

Lines changed: 558 additions & 2 deletions

jest.setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '@testing-library/jest-dom/extend-expect';

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@
149149
],
150150
"modulePathIgnorePatterns": [
151151
"test/e2e/"
152+
],
153+
"setupFilesAfterEnv": [
154+
"<rootDir>/jest.setup.ts"
152155
]
153156
},
154157
"optionalDependencies": {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import { Story, Meta } from '@storybook/react';
3+
import { BaseContainer } from '../../Layout/GlintsContainer/GlintsContainer';
4+
import { CurrencyInputProps, CurrencyInput } from './CurrencyInput';
5+
6+
(CurrencyInput as React.FunctionComponent<CurrencyInputProps>).displayName =
7+
'Currency';
8+
9+
export default {
10+
title: '@next/CurrencyInput',
11+
component: CurrencyInput,
12+
decorators: [Story => <BaseContainer>{Story()}</BaseContainer>],
13+
argTypes: {},
14+
} as Meta;
15+
16+
const Template: Story<CurrencyInputProps> = args => <CurrencyInput {...args} />;
17+
18+
export const Interactive = Template.bind({});
19+
Interactive.args = {
20+
placeholder: '0.0',
21+
disabled: false,
22+
locale: 'en',
23+
value: 10000,
24+
onChange: (value: number) => {
25+
console.log('Currency on changed value: ', value);
26+
},
27+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as React from 'react';
2+
import { fireEvent, render } from '@testing-library/react';
3+
import { CurrencyInput } from './CurrencyInput';
4+
5+
describe('<Currency />', () => {
6+
it('should call onChange with number', async () => {
7+
const onChange = jest.fn();
8+
9+
const screen = render(<CurrencyInput onChange={onChange} />);
10+
const input = screen.getByRole('textbox');
11+
12+
fireEvent.change(input, { target: { value: 1000 } });
13+
14+
const inputEl = screen.getByRole('textbox');
15+
expect(inputEl).toHaveValue('1,000');
16+
17+
expect(onChange).toHaveBeenCalledWith(1000);
18+
});
19+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React, { useState } from 'react';
2+
import { InputProps } from '../Input/Input';
3+
import { StyledCurrency } from './CurrencyStyles';
4+
5+
export type CurrencyInputProps = Omit<
6+
InputProps,
7+
'type' | 'prefix' | 'onChange' | 'value'
8+
> & {
9+
locale?: string;
10+
value?: number;
11+
onChange?: (value: number) => void;
12+
};
13+
14+
export const CurrencyInput = ({
15+
locale = 'en',
16+
value = 0,
17+
onChange,
18+
...props
19+
}: CurrencyInputProps) => {
20+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
21+
// @ts-ignore
22+
const supportedLocale = Intl.ListFormat.supportedLocalesOf(locale);
23+
const localeValue = supportedLocale.length > 0 ? supportedLocale[0] : 'en';
24+
25+
if (supportedLocale.length === 0) {
26+
console.warn(`Locale value of ${locale} is unsupported, "en" will be used`);
27+
}
28+
29+
const formatter = new Intl.NumberFormat(localeValue);
30+
31+
const getRawNumber = (value: string) => {
32+
if (typeof value === 'number') {
33+
return value;
34+
}
35+
36+
const parts = formatter.formatToParts(1000.1);
37+
const thousandSeparator = parts[1].value;
38+
const decimalSeparator = parts[3].value;
39+
40+
const cleanedValue = parseFloat(
41+
value
42+
.replace(new RegExp('\\' + thousandSeparator, 'g'), '')
43+
.replace(new RegExp('\\' + decimalSeparator), '.')
44+
);
45+
46+
return Number.isNaN(cleanedValue) ? 0 : cleanedValue;
47+
};
48+
49+
const [formattedValue, setFormattedValue] = useState(
50+
formatter.format(getRawNumber(value.toString()))
51+
);
52+
53+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
54+
const rawValue = getRawNumber(e.currentTarget.value);
55+
onChange(rawValue);
56+
setFormattedValue(formatter.format(rawValue));
57+
};
58+
59+
return (
60+
<StyledCurrency
61+
type="text"
62+
prefix={<div>$</div>}
63+
{...props}
64+
value={formattedValue === '0' ? '' : formattedValue}
65+
onChange={handleChange}
66+
/>
67+
);
68+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import styled from 'styled-components';
2+
import { Input } from '../Input/Input';
3+
import { space24 } from '../utilities/spacing';
4+
5+
export const StyledCurrency = styled(Input)`
6+
padding-left: ${space24} !important;
7+
`;

src/@next/Icon/icons/icons.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const iconNames = [
6363
'ri-checkbox-line',
6464
'ri-checkbox-indeterminate-fill',
6565
'ri-checkbox-indeterminate-line',
66+
'ri-search',
6667
] as const;
6768

6869
export type IconNames = typeof iconNames[number];
@@ -130,4 +131,5 @@ export const iconsMappingComponent: { [name in IconNames]: SVGComponent } = {
130131
['ri-checkbox-line']: Icons.RiCheckboxLine,
131132
['ri-checkbox-indeterminate-fill']: Icons.RiCheckboxIndeterminateFill,
132133
['ri-checkbox-indeterminate-line']: Icons.RiCheckboxIndeterminateLine,
134+
['ri-search']: Icons.RiSearch,
133135
};

src/@next/Icon/icons/ri-search.svg

Lines changed: 10 additions & 0 deletions
Loading

src/@next/Input/Input.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from 'react';
2+
import {
3+
StyledContainer,
4+
StyledInput,
5+
StyledPrefixContainer,
6+
StyledSuffixContainer,
7+
} from './InputStyle';
8+
9+
export interface InputProps
10+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
11+
error?: boolean;
12+
prefix?: React.ReactNode;
13+
suffix?: React.ReactNode;
14+
}
15+
16+
export const Input = ({
17+
error,
18+
disabled,
19+
prefix,
20+
suffix,
21+
...props
22+
}: InputProps) => {
23+
const hasPrefix = !!prefix;
24+
const hasSuffix = !!suffix;
25+
26+
const Prefix = () =>
27+
hasPrefix ? <StyledPrefixContainer>{prefix}</StyledPrefixContainer> : null;
28+
29+
const Suffix = () =>
30+
hasSuffix ? <StyledSuffixContainer>{suffix}</StyledSuffixContainer> : null;
31+
32+
return (
33+
<StyledContainer
34+
data-prefix={hasPrefix}
35+
data-error={error}
36+
data-disabled={disabled}
37+
>
38+
<Prefix />
39+
<StyledInput disabled={disabled} {...props} />
40+
<Suffix />
41+
</StyledContainer>
42+
);
43+
};

src/@next/Input/InputStyle.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import styled from 'styled-components';
2+
import { Breakpoints } from '../..';
3+
import { Neutral, Red } from '../utilities/colors';
4+
import { InputProps } from './Input';
5+
6+
export const StyledContainer = styled.div<InputProps>`
7+
position: relative;
8+
display: flex;
9+
flex-direction: column;
10+
flex: 1;
11+
flex-basis: 100%;
12+
13+
font-family: 'Noto Sans', sans-serif;
14+
font-style: normal;
15+
font-weight: 400;
16+
font-size: 16px;
17+
line-height: 150%;
18+
19+
&[data-prefix='true'] input {
20+
padding-left: 40px;
21+
}
22+
23+
&[data-error='true'] input {
24+
border: 1px solid ${Red.B93};
25+
}
26+
27+
&[data-error='true'] input:focus {
28+
box-shadow: none;
29+
}
30+
31+
&[data-disabled='true'] input {
32+
border: 1px solid ${Neutral.B85};
33+
background: ${Neutral.B95};
34+
color: ${Neutral.B85};
35+
}
36+
37+
&[data-disabled='true'] input::placeholder {
38+
color: ${Neutral.B85};
39+
}
40+
41+
&[data-disabled='true'] svg {
42+
fill: ${Neutral.B85};
43+
}
44+
45+
&[data-disabled='true'] div {
46+
color: ${Neutral.B85};
47+
}
48+
`;
49+
50+
export const StyledPrefixContainer = styled.div`
51+
position: absolute;
52+
padding: 0;
53+
left: 0;
54+
line-height: 0;
55+
color: ${Neutral.B40};
56+
57+
svg {
58+
height: 17px;
59+
width: 17px;
60+
margin: 10px 14px;
61+
fill: ${Neutral.B40};
62+
}
63+
64+
div {
65+
color: ${Neutral.B40};
66+
margin: 18px 12px;
67+
}
68+
`;
69+
70+
export const StyledSuffixContainer = styled(StyledPrefixContainer)`
71+
left: auto;
72+
right: 0;
73+
`;
74+
75+
export const StyledInput = styled.input<InputProps>`
76+
background: ${Neutral.B100};
77+
box-sizing: border-box;
78+
border: 1px solid ${Neutral.B68};
79+
border-radius: 4px;
80+
padding: 0 12px;
81+
82+
font-family: 'Noto Sans', sans-serif;
83+
font-style: normal;
84+
font-weight: 400;
85+
font-size: 16px;
86+
line-height: 150%;
87+
88+
color: ${Neutral.B18};
89+
90+
flex: none;
91+
order: 1;
92+
align-self: stretch;
93+
flex-grow: 0;
94+
height: 36px;
95+
96+
&::placeholder {
97+
color: ${Neutral.B40};
98+
}
99+
100+
&:focus {
101+
outline: none;
102+
box-shadow: 0px 0px 0px 1px ${Neutral.B100}, 0px 0px 0px 3px #6ac9ec;
103+
}
104+
105+
@media (max-width: ${Breakpoints.large}) {
106+
font-size: 14px;
107+
}
108+
`;

0 commit comments

Comments
 (0)