From 48ba63be6e2538f5702dba30f2d80306b814f6a6 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Wed, 3 Jun 2020 17:57:46 +0200 Subject: [PATCH 1/9] Switch to input type="text" and @wojtekmaj/predict-input-value package to prevent invalid inputs --- package.json | 1 + src/DateInput/Input.jsx | 27 +++++++-------------------- yarn.lock | 8 ++++++++ 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 7db20656..b210cd44 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "dependencies": { "@types/react-calendar": "^3.0.0", "@wojtekmaj/date-utils": "^1.0.3", + "@wojtekmaj/predict-input-value": "^1.0.0", "get-user-locale": "^1.2.0", "make-event-props": "^1.1.0", "merge-class-names": "^1.1.1", diff --git a/src/DateInput/Input.jsx b/src/DateInput/Input.jsx index 2414ed6c..fbd9c72d 100644 --- a/src/DateInput/Input.jsx +++ b/src/DateInput/Input.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import mergeClassNames from 'merge-class-names'; import updateInputWidth, { getFontShorthand } from 'update-input-width'; +import predictInputValue from '@wojtekmaj/predict-input-value'; /* eslint-disable jsx-a11y/no-autofocus */ @@ -45,27 +46,14 @@ function updateInputWidthOnFontLoad(element) { document.fonts.addEventListener('loadingdone', onLoadingDone); } -function getSelectionString() { - if (typeof window === 'undefined') { - return null; - } - - return window.getSelection().toString(); -} - -function makeOnKeyPress(maxLength) { - /** - * Prevents keystrokes that would not produce a number or when value after keystroke would - * exceed maxLength. - */ +function makeOnKeyPress(max) { return function onKeyPress(event) { - const { key, target: input } = event; - const { value } = input; + const { key } = event; const isNumberKey = !isNaN(parseInt(key, 10)); - const selection = getSelectionString(); + const nextValue = predictInputValue(event); - if (isNumberKey && (selection || value.length < maxLength)) { + if (isNumberKey && (nextValue <= max)) { return; } @@ -93,7 +81,6 @@ export default function Input({ value, }) { const hasLeadingZero = showLeadingZeros && value !== null && value < 10; - const maxLength = max.toString().length; return [ (hasLeadingZero && 0), @@ -116,7 +103,7 @@ export default function Input({ onChange={onChange} onFocus={onFocus} onKeyDown={onKeyDown} - onKeyPress={makeOnKeyPress(maxLength)} + onKeyPress={makeOnKeyPress(max)} onKeyUp={(event) => { updateInputWidth(event.target); @@ -137,7 +124,7 @@ export default function Input({ }} required={required} step={step} - type="number" + type="text" value={value !== null ? value : ''} />, ]; diff --git a/yarn.lock b/yarn.lock index 19eeb228..97e2e74a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1760,6 +1760,13 @@ __metadata: languageName: node linkType: hard +"@wojtekmaj/predict-input-value@npm:^1.0.0": + version: 1.0.1 + resolution: "@wojtekmaj/predict-input-value@npm:1.0.1" + checksum: fdbf2a22de81dd3fd23e4a5319bf40c166c705b661bd01dc1084f3c9fad73945ceca45675d42154463447414612a3bf03035f7086e523d7b5d5c442bb31902f9 + languageName: node + linkType: hard + "abab@npm:^2.0.3": version: 2.0.4 resolution: "abab@npm:2.0.4" @@ -6711,6 +6718,7 @@ fsevents@^2.1.2: "@types/react-calendar": ^3.0.0 "@wojtekmaj/date-utils": ^1.0.3 "@wojtekmaj/enzyme-adapter-react-17": ^0.3.1 + "@wojtekmaj/predict-input-value": ^1.0.0 babel-eslint: ^10.0.0 enzyme: ^3.10.0 eslint: ^7.12.0 From 3125b5c053786c3dbc23ffe687eacbb1489a58a5 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Fri, 20 Nov 2020 11:27:04 +0100 Subject: [PATCH 2/9] Implement increment/decrement using arrows --- src/DateInput/Input.jsx | 45 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/DateInput/Input.jsx b/src/DateInput/Input.jsx index fbd9c72d..118305d9 100644 --- a/src/DateInput/Input.jsx +++ b/src/DateInput/Input.jsx @@ -46,6 +46,38 @@ function updateInputWidthOnFontLoad(element) { document.fonts.addEventListener('loadingdone', onLoadingDone); } +function addLeadingZero(value, max) { + `0${value}`.slice(-(`${max}`.length)); +} + +function makeOnKeyDown({ max, min, showLeadingZeros }) { + return function onKeyDown(event) { + switch (event.key) { + case 'ArrowUp': + case 'ArrowDown': { + event.preventDefault(); + + const { target: input } = event; + const { value } = input; + + const rawNextValue = Number(value) + (event.key === 'ArrowUp' ? 1 : -1); + + if (rawNextValue < min || rawNextValue > max) { + return; + } + + const hasLeadingZero = showLeadingZeros && rawNextValue < 10; + const nextValue = hasLeadingZero ? addLeadingZero(rawNextValue, max) : rawNextValue; + + input.value = nextValue; + + break; + } + default: + } + }; +} + function makeOnKeyPress(max) { return function onKeyPress(event) { const { key } = event; @@ -82,6 +114,9 @@ export default function Input({ }) { const hasLeadingZero = showLeadingZeros && value !== null && value < 10; + const onKeyDownInternal = makeOnKeyDown({ max, min, showLeadingZeros }); + const onKeyPressInternal = makeOnKeyPress(max); + return [ (hasLeadingZero && 0), { + onKeyDownInternal(event); + + if (onKeyDown) { + onKeyDown(event); + } + }} + onKeyPress={onKeyPressInternal} onKeyUp={(event) => { updateInputWidth(event.target); From e3edef4565010aa6e0481001cd04e54683e5e100 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Fri, 20 Nov 2020 11:36:11 +0100 Subject: [PATCH 3/9] Implement maxLength in Input --- src/DateInput.jsx | 4 ++-- src/DateInput/Input.jsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/DateInput.jsx b/src/DateInput.jsx index 3c1ed4c7..54c84f10 100644 --- a/src/DateInput.jsx +++ b/src/DateInput.jsx @@ -352,7 +352,7 @@ export default class DateInput extends PureComponent { return; } - const { value } = input; + const { maxLength, value } = input; const max = input.getAttribute('max'); /** @@ -361,7 +361,7 @@ export default class DateInput extends PureComponent { * However, given 2, smallers possible number would be 20, and thus keeping the focus in * this field doesn't make sense. */ - if ((value * 10 > max) || (value.length >= max.length)) { + if ((value * 10 > max) || (value.length >= maxLength)) { const property = 'nextElementSibling'; const nextInput = findInput(input, property); focus(nextInput); diff --git a/src/DateInput/Input.jsx b/src/DateInput/Input.jsx index 118305d9..20e3483c 100644 --- a/src/DateInput/Input.jsx +++ b/src/DateInput/Input.jsx @@ -133,6 +133,7 @@ export default function Input({ disabled={disabled} inputMode="numeric" max={max} + maxLength={`${max}`.length} min={min} name={name} onChange={onChange} From 6ba4c309965768e31ab6b9f17451712cff8e33b3 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Fri, 20 Nov 2020 11:57:43 +0100 Subject: [PATCH 4/9] Keep values in state as strings --- src/DateInput.jsx | 8 +++---- src/DateInput.spec.jsx | 36 +++++++++++++++--------------- src/DateInput/DayInput.jsx | 10 +++++---- src/DateInput/DayInput.spec.jsx | 2 +- src/DateInput/Input.jsx | 10 +++++++-- src/DateInput/MonthInput.jsx | 8 ++++--- src/DateInput/MonthInput.spec.jsx | 2 +- src/DateInput/MonthSelect.jsx | 8 ++++--- src/DateInput/MonthSelect.spec.jsx | 2 +- src/DateInput/YearInput.jsx | 4 +++- src/DateInput/YearInput.spec.jsx | 2 +- 11 files changed, 53 insertions(+), 39 deletions(-) diff --git a/src/DateInput.jsx b/src/DateInput.jsx index 54c84f10..3939cd91 100644 --- a/src/DateInput.jsx +++ b/src/DateInput.jsx @@ -192,9 +192,9 @@ export default class DateInput extends PureComponent { ) ) { if (nextValue) { - nextState.year = getYear(nextValue); - nextState.month = getMonthHuman(nextValue); - nextState.day = getDate(nextValue); + nextState.year = getYear(nextValue).toString(); + nextState.month = getMonthHuman(nextValue).toString(); + nextState.day = getDate(nextValue).toString(); } else { nextState.year = null; nextState.month = null; @@ -375,7 +375,7 @@ export default class DateInput extends PureComponent { const { name, value } = event.target; this.setState( - { [name]: value ? parseInt(value, 10) : null }, + { [name]: value }, this.onChangeExternal, ); } diff --git a/src/DateInput.spec.jsx b/src/DateInput.spec.jsx index e17baf89..7fd86be1 100644 --- a/src/DateInput.spec.jsx +++ b/src/DateInput.spec.jsx @@ -115,9 +115,9 @@ describe('DateInput', () => { const customInputs = component.find('input[data-input]'); expect(nativeInput.prop('value')).toBe('2017-09-30'); - expect(customInputs.at(0).prop('value')).toBe(9); - expect(customInputs.at(1).prop('value')).toBe(30); - expect(customInputs.at(2).prop('value')).toBe(2017); + expect(customInputs.at(0).prop('value')).toBe('9'); + expect(customInputs.at(1).prop('value')).toBe('30'); + expect(customInputs.at(2).prop('value')).toBe('2017'); }); it('shows a given date in all inputs correctly given array of Date objects (12-hour format)', () => { @@ -134,9 +134,9 @@ describe('DateInput', () => { const customInputs = component.find('input[data-input]'); expect(nativeInput.prop('value')).toBe('2017-09-30'); - expect(customInputs.at(0).prop('value')).toBe(9); - expect(customInputs.at(1).prop('value')).toBe(30); - expect(customInputs.at(2).prop('value')).toBe(2017); + expect(customInputs.at(0).prop('value')).toBe('9'); + expect(customInputs.at(1).prop('value')).toBe('30'); + expect(customInputs.at(2).prop('value')).toBe('2017'); }); it('shows a given date in all inputs correctly given ISO string (12-hour format)', () => { @@ -153,9 +153,9 @@ describe('DateInput', () => { const customInputs = component.find('input[data-input]'); expect(nativeInput.prop('value')).toBe('2017-09-30'); - expect(customInputs.at(0).prop('value')).toBe(9); - expect(customInputs.at(1).prop('value')).toBe(30); - expect(customInputs.at(2).prop('value')).toBe(2017); + expect(customInputs.at(0).prop('value')).toBe('9'); + expect(customInputs.at(1).prop('value')).toBe('30'); + expect(customInputs.at(2).prop('value')).toBe('2017'); }); itIfFullICU('shows a given date in all inputs correctly given Date (24-hour format)', () => { @@ -173,9 +173,9 @@ describe('DateInput', () => { const customInputs = component.find('input[data-input]'); expect(nativeInput.prop('value')).toBe('2017-09-30'); - expect(customInputs.at(0).prop('value')).toBe(2017); - expect(customInputs.at(1).prop('value')).toBe(9); - expect(customInputs.at(2).prop('value')).toBe(30); + expect(customInputs.at(0).prop('value')).toBe('2017'); + expect(customInputs.at(1).prop('value')).toBe('9'); + expect(customInputs.at(2).prop('value')).toBe('30'); }); itIfFullICU('shows a given date in all inputs correctly given array of Date objects (24-hour format)', () => { @@ -193,9 +193,9 @@ describe('DateInput', () => { const customInputs = component.find('input[data-input]'); expect(nativeInput.prop('value')).toBe('2017-09-30'); - expect(customInputs.at(0).prop('value')).toBe(2017); - expect(customInputs.at(1).prop('value')).toBe(9); - expect(customInputs.at(2).prop('value')).toBe(30); + expect(customInputs.at(0).prop('value')).toBe('2017'); + expect(customInputs.at(1).prop('value')).toBe('9'); + expect(customInputs.at(2).prop('value')).toBe('30'); }); itIfFullICU('shows a given date in all inputs correctly given ISO string (24-hour format)', () => { @@ -213,9 +213,9 @@ describe('DateInput', () => { const customInputs = component.find('input[data-input]'); expect(nativeInput.prop('value')).toBe('2017-09-30'); - expect(customInputs.at(0).prop('value')).toBe(2017); - expect(customInputs.at(1).prop('value')).toBe(9); - expect(customInputs.at(2).prop('value')).toBe(30); + expect(customInputs.at(0).prop('value')).toBe('2017'); + expect(customInputs.at(1).prop('value')).toBe('9'); + expect(customInputs.at(2).prop('value')).toBe('30'); }); it('shows empty value in all inputs correctly given null', () => { diff --git a/src/DateInput/DayInput.jsx b/src/DateInput/DayInput.jsx index 05f697ac..b3a9e2f1 100644 --- a/src/DateInput/DayInput.jsx +++ b/src/DateInput/DayInput.jsx @@ -28,7 +28,7 @@ export default function DayInput({ })(); function isSameMonth(date) { - return date && year === getYear(date) && month === getMonthHuman(date); + return date && Number(year) === getYear(date) && Number(month) === getMonthHuman(date); } const maxDay = safeMin(currentMonthMaxDays, isSameMonth(maxDate) && getDate(maxDate)); @@ -44,6 +44,8 @@ export default function DayInput({ ); } +const isValue = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); + DayInput.propTypes = { ariaLabel: PropTypes.string, className: PropTypes.string.isRequired, @@ -51,13 +53,13 @@ DayInput.propTypes = { itemRef: PropTypes.func, maxDate: isMaxDate, minDate: isMinDate, - month: PropTypes.number, + month: isValue, onChange: PropTypes.func, onKeyDown: PropTypes.func, onKeyUp: PropTypes.func, placeholder: PropTypes.string, required: PropTypes.bool, showLeadingZeros: PropTypes.bool, - value: PropTypes.number, - year: PropTypes.number, + value: isValue, + year: isValue, }; diff --git a/src/DateInput/DayInput.spec.jsx b/src/DateInput/DayInput.spec.jsx index 86378fab..102133aa 100644 --- a/src/DateInput/DayInput.spec.jsx +++ b/src/DateInput/DayInput.spec.jsx @@ -120,7 +120,7 @@ describe('DayInput', () => { }); it('displays given value properly', () => { - const value = 11; + const value = '11'; const component = mount( { }); it('displays given value properly', () => { - const value = 11; + const value = '11'; const component = mount( { }); it('displays given value properly', () => { - const value = 11; + const value = '11'; const component = mount( { }); it('displays given value properly', () => { - const value = 2018; + const value = '2018'; const component = mount( Date: Fri, 20 Nov 2020 11:59:51 +0100 Subject: [PATCH 5/9] Change parseInt() to Number() --- src/DateInput.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/DateInput.jsx b/src/DateInput.jsx index 3939cd91..195e3453 100644 --- a/src/DateInput.jsx +++ b/src/DateInput.jsx @@ -397,9 +397,9 @@ export default class DateInput extends PureComponent { } const [yearString, monthString, dayString] = value.split('-'); - const year = parseInt(yearString, 10); - const monthIndex = parseInt(monthString, 10) - 1 || 0; - const day = parseInt(dayString, 10) || 1; + const year = Number(yearString); + const monthIndex = Number(monthString) - 1 || 0; + const day = Number(dayString) || 1; const proposedValue = new Date(); proposedValue.setFullYear(year, monthIndex, day); @@ -434,9 +434,9 @@ export default class DateInput extends PureComponent { } else if ( formElements.every((formElement) => formElement.value && formElement.validity.valid) ) { - const year = parseInt(values.year, 10); - const monthIndex = parseInt(values.month, 10) - 1 || 0; - const day = parseInt(values.day || 1, 10); + const year = Number(values.year); + const monthIndex = Number(values.month) - 1 || 0; + const day = Number(values.day || 1); const proposedValue = new Date(); proposedValue.setFullYear(year, monthIndex, day); From 35535ebfd32e90b50bbc39cebef0e1e1a3f86746 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Fri, 20 Nov 2020 13:04:24 +0100 Subject: [PATCH 6/9] Trigger onChange when input is changed using arrows --- src/DateInput/Input.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/DateInput/Input.jsx b/src/DateInput/Input.jsx index befc2584..244ecfa3 100644 --- a/src/DateInput/Input.jsx +++ b/src/DateInput/Input.jsx @@ -50,7 +50,9 @@ function addLeadingZero(value, max) { `0${value}`.slice(-(`${max}`.length)); } -function makeOnKeyDown({ max, min, showLeadingZeros }) { +function makeOnKeyDown({ + max, min, onChange, showLeadingZeros, +}) { return function onKeyDown(event) { switch (event.key) { case 'ArrowUp': @@ -70,6 +72,7 @@ function makeOnKeyDown({ max, min, showLeadingZeros }) { const nextValue = hasLeadingZero ? addLeadingZero(rawNextValue, max) : rawNextValue; input.value = nextValue; + onChange(event); break; } @@ -118,7 +121,9 @@ export default function Input({ && value.toString().length < 2 ); - const onKeyDownInternal = makeOnKeyDown({ max, min, showLeadingZeros }); + const onKeyDownInternal = makeOnKeyDown({ + max, min, onChange, showLeadingZeros, + }); const onKeyPressInternal = makeOnKeyPress(max); return [ From 2d25a9354810c79764c4f0796dd9a629f1073496 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Fri, 20 Nov 2020 13:08:16 +0100 Subject: [PATCH 7/9] Validate input manually before creating date out of it --- src/DateInput.jsx | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/DateInput.jsx b/src/DateInput.jsx index 195e3453..527b66c4 100644 --- a/src/DateInput.jsx +++ b/src/DateInput.jsx @@ -108,6 +108,28 @@ function findInput(element, property) { return nextElement; } +function isInputValid(input) { + if (!input.validity.valid) { + return false; + } + + const { value } = input; + + if (!value) { + return false; + } + + const rawValue = Number(value); + const min = Number(input.getAttribute('min')); + const max = Number(input.getAttribute('max')); + + if (rawValue < min || rawValue > max) { + return false; + } + + return true; +} + function focus(element) { if (element) { element.focus(); @@ -431,9 +453,7 @@ export default class DateInput extends PureComponent { if (formElements.every((formElement) => !formElement.value)) { onChange(null, false); - } else if ( - formElements.every((formElement) => formElement.value && formElement.validity.valid) - ) { + } else if (formElements.every(isInputValid)) { const year = Number(values.year); const monthIndex = Number(values.month) - 1 || 0; const day = Number(values.day || 1); From 4fcb4589109b113c1b0b65fcd08fdade90f4e842 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Wed, 10 Mar 2021 15:39:48 +0100 Subject: [PATCH 8/9] Fixup Implement increment/decrement using arrows --- src/DateInput/Input.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DateInput/Input.jsx b/src/DateInput/Input.jsx index 244ecfa3..63d9cbd7 100644 --- a/src/DateInput/Input.jsx +++ b/src/DateInput/Input.jsx @@ -47,7 +47,7 @@ function updateInputWidthOnFontLoad(element) { } function addLeadingZero(value, max) { - `0${value}`.slice(-(`${max}`.length)); + return `0${value}`.slice(-(`${max}`.length)); } function makeOnKeyDown({ From e108ce4f88fb87aae3a6830d17126b26c9a0f124 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Wed, 10 Mar 2021 16:32:16 +0100 Subject: [PATCH 9/9] Support incrementing/decrementing using arrows if current value is outside of min/max range --- src/DateInput/Input.jsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/DateInput/Input.jsx b/src/DateInput/Input.jsx index 63d9cbd7..744d66cd 100644 --- a/src/DateInput/Input.jsx +++ b/src/DateInput/Input.jsx @@ -62,14 +62,16 @@ function makeOnKeyDown({ const { target: input } = event; const { value } = input; - const rawNextValue = Number(value) + (event.key === 'ArrowUp' ? 1 : -1); + const numericValue = Number(value); + const rawNextValue = numericValue + (event.key === 'ArrowUp' ? 1 : -1); - if (rawNextValue < min || rawNextValue > max) { - return; - } + const limitedRawNextValue = Math.min(max, Math.max(min, rawNextValue)); - const hasLeadingZero = showLeadingZeros && rawNextValue < 10; - const nextValue = hasLeadingZero ? addLeadingZero(rawNextValue, max) : rawNextValue; + const hasLeadingZero = showLeadingZeros && limitedRawNextValue < 10; + const nextValue = (hasLeadingZero + ? addLeadingZero(limitedRawNextValue, max) + : limitedRawNextValue + ); input.value = nextValue; onChange(event);