Skip to content

fix(money-input): caret position in integer part #1096

Merged
merged 11 commits into from
Apr 17, 2020
20 changes: 20 additions & 0 deletions src/money-input/money-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,24 @@ describe('money-input', () => {

expect(moneyInput.find('input').prop('value')).toBe('1 234,56');
});

it('should stay caret before comma', (done) => {
const moneyInput = mount<MoneyInput>(<MoneyInput value='12,34' />);
const inputNode = moneyInput.find('input');

setTimeout(() => {
jest.useFakeTimers();

inputNode.getDOMNode<HTMLInputElement>().selectionStart = 3;
inputNode.getDOMNode<HTMLInputElement>().selectionEnd = 3;

inputNode.simulate('beforeInput');
inputNode.simulate('input', { target: { value: '123,34' } });
jest.runAllTimers();

expect(inputNode.getDOMNode<HTMLInputElement>().selectionStart).toBe(3);
expect(inputNode.getDOMNode<HTMLInputElement>().selectionEnd).toBe(3);
done();
}, 0);
});
});
70 changes: 68 additions & 2 deletions src/money-input/money-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const DEFAULT_FRACTION_SIZE = 2;
const DEFAULT_INTEGER_SIZE = 9;
const INTEGER_PART_SIZE = 3;

// В эту проверку попадают IE9 и IE10, которые не могут корректно работать с кареткой на событии `input`.
const IS_IE9_10 = typeof window !== 'undefined' && !!(window as any).ActiveXObject;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это надо убрать. IE 11

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Убрал


const IS_ANDROID = typeof window !== 'undefined' && /(android)/i.test(window.navigator.userAgent);

/**
* Возвращает целую и дробную часть значения в виде массива.
* Если дробная часть не равна `undefined`, значит введена дробная часть
Expand Down Expand Up @@ -106,6 +111,8 @@ export class MoneyInput extends React.PureComponent<MoneyInputProps, MoneyInputS

root: HTMLInputElement;

private caretFixTimeout: ReturnType<typeof setTimeout> = null;

// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this.updateMaskByValue(this.getValue());
Expand All @@ -117,6 +124,12 @@ export class MoneyInput extends React.PureComponent<MoneyInputProps, MoneyInputS
this.updateMaskByValue(nextProps.value || '');
}
}
componentWillUnmount() {
if (this.caretFixTimeout) {
clearTimeout(this.caretFixTimeout);
this.caretFixTimeout = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Для чего эта проверка и установка прям перед дестроем этого флага в null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Переносил часть кода из masked-input.
Упростил

}
}

render() {
return (
Expand Down Expand Up @@ -155,11 +168,13 @@ export class MoneyInput extends React.PureComponent<MoneyInputProps, MoneyInputS
}

private handleProcessMaskInputEvent = (event) => {
const currentValue = this.mask.format(this.getValue());
const currentValue = this.getValue();
const currentFormattedValue = this.mask.format(this.getValue());
let newValue = event.target.value;
const currentSelection = this.root.control.input.selectionStart;

// При удалении отрезаем запятую, если исчезла дробная часть.
if (newValue.length < currentValue.length) {
if (newValue.length < currentFormattedValue.length) {
const fractionPart = getValueParts(newValue)[1]; // Берем значение после запятой

// `fractionPart !== undefined` - значит запятая введена, но
Expand All @@ -171,6 +186,19 @@ export class MoneyInput extends React.PureComponent<MoneyInputProps, MoneyInputS
}

this.updateMaskByValue(newValue);

// При добавлении последней цифры целой части каретка должна
// оставаться перед запятой
if (newValue.length > currentValue.length && newValue[currentSelection] === ',') {
setTimeout((() => {

// Фикс бага смещения каретки в браузере на андроидах Jelly Bean (c 4.1 по 4.3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

мы поддерживаем такие старые версии?
это вроде как устаревшие версии, которые были актулальны в 2013 (7 лет назад).
Есть какая-то статистика, в которой отображено сколько людей к нам заходит со старыми версиями андроида?

const offsetSection = IS_ANDROID && parseFloat(getAndroidVersion() as string) < 4.4 ? 1 : 0;
const newSelectionStart = this.root.control.input.selectionStart - 1;

this.setInputSelection(newSelectionStart + offsetSection);
}), 0);
}
};

private handleChange = (value) => {
Expand Down Expand Up @@ -246,6 +274,44 @@ export class MoneyInput extends React.PureComponent<MoneyInputProps, MoneyInputS
private getValue() {
return this.props.value === undefined ? this.state.value : this.props.value;
}

/**
* Устанавливает каретку поля ввода в новую позицию.
*
* @param selection Новое положение каретки
*/
private setInputSelection(selection: number) {
this.root.control.input.selectionStart = selection;
this.root.control.input.selectionEnd = selection;
// IE10 не умеет синхронно в событие `change` переставлять каретку.
// Android chrome имеет дефект с автоматической установкой каретки
// при использовании клавиатуры отличной от type="text".
if (IS_IE9_10 || IS_ANDROID) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SiebenSieben IE11 у нас же уже

this.setInputSelectionByTimeout(selection);
}
}

/**
* Устанавливает каретку поля ввода в заданную позицию асинхронно.
*
* Во-избежание дефекта с установкой каретки, наблюдаемом в мобильных браузерах, а так же
* браузерах IE9-10, установка происходит асинхронно, с минимальной задержкой,
* с помощью [setTimeout] обертки.
*
* @param selection Положение каретки
*/
private setInputSelectionByTimeout(selection: number) {
if (this.caretFixTimeout) {
clearTimeout(this.caretFixTimeout);
this.caretFixTimeout = null;
}

this.caretFixTimeout = setTimeout(() => {
this.caretFixTimeout = null;
this.root.control.input.selectionStart = selection;
this.root.control.input.selectionEnd = selection;
}, 0);
}
}

class ThemedMoneyInput extends MoneyInput {}
Expand Down