Skip to content

Commit

Permalink
Merge pull request #91 from cchanxzy/feat/handle-key-steps
Browse files Browse the repository at this point in the history
feat: handle arrow down and arrow up step changes
  • Loading branch information
cchanxzy authored Nov 15, 2020
2 parents ec5186e + 31e6156 commit 689377b
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 39 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ Example if `fixedDecimalLength` was 2:
| id | `string` | | Component id |
| maxLength | `number` | | Maximum characters the user can enter |
| onChange | `function` | | Handle change in value |
| onBlurValue | `function` | | Handle value onBlur |
| placeholder | `string` | | Placeholder if no value |
| precision | `number` | | Specify decimal precision for padding/trimming |
| prefix | `string` | | Include a prefix eg. £ or \$ |
| step | `number` | | Incremental value change on arrow down and arrow up key press |
| decimalSeparator | `string` | `.` | Separator between integer part and fractional part of value |
| groupSeparator | `string` | `,` | Separator between thousand, million and billion |
| turnOffSeparators | `boolean` | `false` | Disable auto adding the group separator between values, eg. 1000 > 1,000 |
Expand Down
43 changes: 34 additions & 9 deletions src/components/CurrencyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ export const CurrencyInput: FC<CurrencyInputProps> = ({
defaultValue,
disabled = false,
maxLength: userMaxLength,
value,
value: userValue,
onChange,
onBlurValue,
fixedDecimalLength,
placeholder,
precision,
prefix,
step,
decimalSeparator = '.',
groupSeparator = ',',
turnOffSeparators = false,
Expand Down Expand Up @@ -62,18 +63,16 @@ export const CurrencyInput: FC<CurrencyInputProps> = ({

const onFocus = (): number => (stateValue ? stateValue.length : 0);

const processChange = ({
target: { value, selectionStart },
}: React.ChangeEvent<HTMLInputElement>): void => {
const valueOnly = cleanValue({ value: String(value), ...cleanValueOptions });
const processChange = (value: string, selectionStart?: number | null): void => {
const valueOnly = cleanValue({ value, ...cleanValueOptions });

if (!valueOnly) {
onChange && onChange(undefined, name);
setStateValue('');
return;
}

if (userMaxLength && valueOnly.length > userMaxLength) {
if (userMaxLength && valueOnly.replace(/-/g, '').length > userMaxLength) {
return;
}

Expand All @@ -86,7 +85,7 @@ export const CurrencyInput: FC<CurrencyInputProps> = ({
const formattedValue = formatValue({ value: valueOnly, ...formatValueOptions });

/* istanbul ignore next */
if (selectionStart) {
if (selectionStart !== undefined && selectionStart !== null) {
const cursor = selectionStart + (formattedValue.length - value.length) || 1;
setCursor(cursor);
}
Expand All @@ -96,6 +95,12 @@ export const CurrencyInput: FC<CurrencyInputProps> = ({
onChange && onChange(valueOnly, name);
};

const handleOnChange = ({
target: { value, selectionStart },
}: React.ChangeEvent<HTMLInputElement>): void => {
processChange(value, selectionStart);
};

const handleOnBlur = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>): void => {
const valueOnly = cleanValue({ value, ...cleanValueOptions });

Expand All @@ -116,6 +121,23 @@ export const CurrencyInput: FC<CurrencyInputProps> = ({
setStateValue(formattedValue);
};

const handleOnKeyDown = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
if (step && (key === 'ArrowUp' || key === 'ArrowDown')) {
const currentValue =
Number(
userValue !== undefined
? userValue
: cleanValue({ value: stateValue, ...cleanValueOptions })
) || 0;
const newValue =
key === 'ArrowUp'
? String(currentValue + Number(step))
: String(currentValue - Number(step));

processChange(newValue);
}
};

/* istanbul ignore next */
useEffect(() => {
if (inputRef && inputRef.current) {
Expand All @@ -124,7 +146,9 @@ export const CurrencyInput: FC<CurrencyInputProps> = ({
}, [cursor, inputRef]);

const formattedPropsValue =
value !== undefined ? formatValue({ value: String(value), ...formatValueOptions }) : undefined;
userValue !== undefined
? formatValue({ value: String(userValue), ...formatValueOptions })
: undefined;

return (
<input
Expand All @@ -133,9 +157,10 @@ export const CurrencyInput: FC<CurrencyInputProps> = ({
id={id}
name={name}
className={className}
onChange={processChange}
onChange={handleOnChange}
onBlur={handleOnBlur}
onFocus={onFocus}
onKeyDown={handleOnKeyDown}
placeholder={placeholder}
disabled={disabled}
value={
Expand Down
5 changes: 5 additions & 0 deletions src/components/CurrencyInputProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export type CurrencyInputProps = Overwrite<
*/
prefix?: string;

/**
* Incremental value change on arrow down and arrow up key press
*/
step?: number;

/**
* Separator between integer part and fractional part of value. Cannot be a number
*
Expand Down
155 changes: 155 additions & 0 deletions src/components/__tests__/CurrencyInput-handleKeyDown.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { shallow } from 'enzyme';
import React from 'react';
import CurrencyInput from '../CurrencyInput';

const id = 'validationCustom01';

describe('<CurrencyInput /> component > handleKeyDown', () => {
const onChangeSpy = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it('should not change value if no step prop', () => {
const view = shallow(
<CurrencyInput id={id} prefix="£" defaultValue={100} onChange={onChangeSpy} />
);

// Arrow up
view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' });
expect(onChangeSpy).not.toBeCalled();
expect(view.update().find(`#${id}`).prop('value')).toBe('£100');

// Arrow down
view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' });
expect(onChangeSpy).not.toBeCalled();
expect(view.update().find(`#${id}`).prop('value')).toBe('£100');
});

it('should handle negative step', () => {
const view = shallow(
<CurrencyInput id={id} prefix="£" defaultValue={100} step={-2} onChange={onChangeSpy} />
);

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' });
expect(onChangeSpy).toHaveBeenCalledWith('98', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('£98');

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' });
expect(onChangeSpy).toHaveBeenCalledWith('100', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('£100');
});

describe('without value ie. default 0', () => {
it('should handle arrow down key', () => {
const view = shallow(<CurrencyInput id={id} prefix="£" step={1} onChange={onChangeSpy} />);

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' });
expect(onChangeSpy).toBeCalledWith('-1', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('-£1');
});

it('should handle arrow down key', () => {
const view = shallow(<CurrencyInput id={id} prefix="£" step={1} onChange={onChangeSpy} />);

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' });
expect(onChangeSpy).toBeCalledWith('1', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('£1');
});
});

describe('with value 99 and step 1.25', () => {
it('should handle arrow down key', () => {
const view = shallow(
<CurrencyInput id={id} prefix="£" value={99} step={1.25} onChange={onChangeSpy} />
);

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' });
expect(onChangeSpy).toHaveBeenCalledWith('97.75', undefined);
});

it('should handle arrow up key', () => {
const view = shallow(
<CurrencyInput id={id} prefix="£" value={99} step={1.25} onChange={onChangeSpy} />
);

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' });
expect(onChangeSpy).toHaveBeenCalledWith('100.25', undefined);
});
});

describe('with defaultValue 100 and step 5.5', () => {
it('should handle arrow down key', () => {
const view = shallow(
<CurrencyInput id={id} prefix="£" defaultValue={100} step={5.5} onChange={onChangeSpy} />
);

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' });
expect(onChangeSpy).toBeCalledWith('94.5', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('£94.5');

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' });
expect(onChangeSpy).toBeCalledWith('89', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('£89');
});

it('should handle arrow up key', () => {
const view = shallow(
<CurrencyInput id={id} prefix="£" defaultValue={100} step={5.5} onChange={onChangeSpy} />
);

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' });
expect(onChangeSpy).toBeCalledWith('105.5', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('£105.5');

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' });
expect(onChangeSpy).toBeCalledWith('111', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('£111');
});
});

describe('with max length 2', () => {
it('should handle negative value', () => {
const view = shallow(
<CurrencyInput
id={id}
prefix="£"
defaultValue={-99}
step={1}
maxLength={2}
onChange={onChangeSpy}
/>
);

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' });
expect(onChangeSpy).not.toBeCalled();
expect(view.update().find(`#${id}`).prop('value')).toBe('-£99');

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' });
expect(onChangeSpy).toHaveBeenCalledWith('-98', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('-£98');
});

it('should handle positive value', () => {
const view = shallow(
<CurrencyInput
id={id}
prefix="£"
defaultValue={99}
step={1}
maxLength={2}
onChange={onChangeSpy}
/>
);

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' });
expect(onChangeSpy).not.toBeCalled();
expect(view.update().find(`#${id}`).prop('value')).toBe('£99');

view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' });
expect(onChangeSpy).toHaveBeenCalledWith('98', undefined);
expect(view.update().find(`#${id}`).prop('value')).toBe('£98');
});
});
});
59 changes: 59 additions & 0 deletions src/components/__tests__/CurrencyInput-maxLength.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { shallow } from 'enzyme';
import React from 'react';
import CurrencyInput from '../CurrencyInput';

const id = 'validationCustom01';

describe('<CurrencyInput /> component > maxLength', () => {
const onChangeSpy = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it('should not allow more values than max length', () => {
const view = shallow(
<CurrencyInput
id={id}
name={name}
prefix="£"
onChange={onChangeSpy}
maxLength={3}
defaultValue={123}
/>
);

const input = view.find(`#${id}`);
expect(input.prop('value')).toBe('£123');

input.simulate('change', { target: { value: '£1234' } });
expect(onChangeSpy).not.toBeCalled();

const updatedView = view.update();
expect(updatedView.find(`#${id}`).prop('value')).toBe('£123');
});

it('should apply max length rule to negative value', () => {
const view = shallow(
<CurrencyInput
id={id}
name={name}
prefix="£"
onChange={onChangeSpy}
maxLength={3}
defaultValue={-123}
/>
);

const input = view.find(`#${id}`);
expect(input.prop('value')).toBe('-£123');

input.simulate('change', { target: { value: '-£1234' } });
expect(onChangeSpy).not.toBeCalled();
expect(view.update().find(`#${id}`).prop('value')).toBe('-£123');

input.simulate('change', { target: { value: '-£125' } });
expect(onChangeSpy).toHaveBeenCalledWith('-125', '');
expect(view.update().find(`#${id}`).prop('value')).toBe('-£125');
});
});
Loading

0 comments on commit 689377b

Please sign in to comment.