diff --git a/packages/react-core/src/components/NumberInput/NumberInput.tsx b/packages/react-core/src/components/NumberInput/NumberInput.tsx index 80e04f4ad71..d4d3ed43954 100644 --- a/packages/react-core/src/components/NumberInput/NumberInput.tsx +++ b/packages/react-core/src/components/NumberInput/NumberInput.tsx @@ -10,7 +10,7 @@ import { TextInput } from '../TextInput'; export interface NumberInputProps extends React.HTMLProps { /** Value of the number input */ - value?: number | null; + value?: number | ''; /** Additional classes added to the number input */ className?: string; /** Sets the width of the number input to a number of characters */ @@ -89,7 +89,6 @@ export const NumberInput: React.FunctionComponent = ({ plusBtnProps, ...props }: NumberInputProps) => { - value = value || 0; const numberInputUnit =
{unit}
; const keyDownHandler = inputProps && inputProps.onKeyDown ? inputProps.onKeyDown : defaultKeyDownHandler({ inputName, onMinus, onPlus }); diff --git a/packages/react-core/src/components/NumberInput/__tests__/NumberInput.test.tsx b/packages/react-core/src/components/NumberInput/__tests__/NumberInput.test.tsx index 3f079d3fa59..c2bc167c1f4 100644 --- a/packages/react-core/src/components/NumberInput/__tests__/NumberInput.test.tsx +++ b/packages/react-core/src/components/NumberInput/__tests__/NumberInput.test.tsx @@ -195,22 +195,22 @@ describe('numberInput', () => { expect(input).toHaveDisplayValue('47.01'); }); - test('renders 0 if no value passed', () => { + test('renders 0 (default value) if no value passed', () => { render(); const input = screen.getByRole('spinbutton'); expect(input).toHaveDisplayValue('0'); }); - test('renders 0 if undefined value passed', () => { + test('renders 0 (default value) if undefined value passed', () => { render(); const input = screen.getByRole('spinbutton'); expect(input).toHaveDisplayValue('0'); }); - test('renders 0 if null value passed', () => { + test('renders nothing if null value passed', () => { render(); const input = screen.getByRole('spinbutton'); - expect(input).toHaveDisplayValue('0'); + expect(input).toHaveDisplayValue(''); }); test('does not throw an error if onChange is passed via inputProps as well as the onChange prop', () => { diff --git a/packages/react-core/src/components/NumberInput/examples/NumberInput.md b/packages/react-core/src/components/NumberInput/examples/NumberInput.md index ef7f961cdd0..9e90f62a691 100644 --- a/packages/react-core/src/components/NumberInput/examples/NumberInput.md +++ b/packages/react-core/src/components/NumberInput/examples/NumberInput.md @@ -19,6 +19,8 @@ propComponents: ['NumberInput'] ### With unit and thresholds +To enable a user entered value to snap to the nearest threshold if the entered input is out of bounds, define the blur event handler. + ```ts file="./NumberInputUnitThreshold.tsx" ``` diff --git a/packages/react-core/src/components/NumberInput/examples/NumberInputCustomStep.tsx b/packages/react-core/src/components/NumberInput/examples/NumberInputCustomStep.tsx index d46391fa724..c63f02b754b 100644 --- a/packages/react-core/src/components/NumberInput/examples/NumberInputCustomStep.tsx +++ b/packages/react-core/src/components/NumberInput/examples/NumberInputCustomStep.tsx @@ -2,16 +2,16 @@ import React from 'react'; import { NumberInput } from '@patternfly/react-core'; export const NumberInputCustomStep: React.FunctionComponent = () => { - const [value, setValue] = React.useState(90); + const [value, setValue] = React.useState(90); const step = 3; const stepper = (stepValue: number) => { - setValue(value + stepValue); + setValue((value || 0) + stepValue); }; const onChange = (event: React.FormEvent) => { - const target = event.target as HTMLInputElement; - setValue(Number(target.value)); + const value = (event.target as HTMLInputElement).value; + setValue(value === '' ? value : +value); }; return ( diff --git a/packages/react-core/src/components/NumberInput/examples/NumberInputCustomStepAndThreshold.tsx b/packages/react-core/src/components/NumberInput/examples/NumberInputCustomStepAndThreshold.tsx index a77f875dc3a..f410c255616 100644 --- a/packages/react-core/src/components/NumberInput/examples/NumberInputCustomStepAndThreshold.tsx +++ b/packages/react-core/src/components/NumberInput/examples/NumberInputCustomStepAndThreshold.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { NumberInput } from '@patternfly/react-core'; export const NumberInputCustomStepAndThreshold: React.FunctionComponent = () => { - const [value, setValue] = React.useState(90); + const [value, setValue] = React.useState(90); const minValue = 90; const maxValue = 100; const step = 3; @@ -19,13 +19,12 @@ export const NumberInputCustomStepAndThreshold: React.FunctionComponent = () => }; const stepper = (stepValue: number) => { - setValue(value + stepValue); + setValue((value || 0) + stepValue); }; const onChange = (event: React.FormEvent) => { - const target = event.target as HTMLInputElement; - const newValue = normalizeBetween(isNaN(+target.value) ? 0 : Number(target.value), minValue, maxValue); - setValue(newValue); + const value = (event.target as HTMLInputElement).value; + setValue(value === '' ? value : +value); }; const onBlur = (event: React.FormEvent) => { diff --git a/packages/react-core/src/components/NumberInput/examples/NumberInputDefault.tsx b/packages/react-core/src/components/NumberInput/examples/NumberInputDefault.tsx index a3e973a79f2..4f86471db04 100644 --- a/packages/react-core/src/components/NumberInput/examples/NumberInputDefault.tsx +++ b/packages/react-core/src/components/NumberInput/examples/NumberInputDefault.tsx @@ -2,20 +2,20 @@ import React from 'react'; import { NumberInput } from '@patternfly/react-core'; export const NumberInputDefault: React.FunctionComponent = () => { - const [value, setValue] = React.useState(90); + const [value, setValue] = React.useState(90); const onMinus = () => { - const newValue = value - 1; + const newValue = (value || 0) - 1; setValue(newValue); }; const onChange = (event: React.FormEvent) => { - const target = event.target as HTMLInputElement; - setValue(Number(target.value)); + const value = (event.target as HTMLInputElement).value; + setValue(value === '' ? value : +value); }; const onPlus = () => { - const newValue = value + 1; + const newValue = (value || 0) + 1; setValue(newValue); }; diff --git a/packages/react-core/src/components/NumberInput/examples/NumberInputDisabled.tsx b/packages/react-core/src/components/NumberInput/examples/NumberInputDisabled.tsx index 20b35f919d2..5f182ac00df 100644 --- a/packages/react-core/src/components/NumberInput/examples/NumberInputDisabled.tsx +++ b/packages/react-core/src/components/NumberInput/examples/NumberInputDisabled.tsx @@ -2,22 +2,22 @@ import React from 'react'; import { NumberInput } from '@patternfly/react-core'; export const NumberInputDisabled: React.FunctionComponent = () => { - const [value, setValue] = React.useState(100); + const [value, setValue] = React.useState(100); const minValue = 0; const maxValue = 100; const onMinus = () => { - const newValue = value - 1; + const newValue = (value || 0) - 1; setValue(newValue); }; const onChange = (event: React.FormEvent) => { - const target = event.target as HTMLInputElement; - setValue(Number(target.value)); + const value = (event.target as HTMLInputElement).value; + setValue(value === '' ? value : +value); }; const onPlus = () => { - const newValue = value + 1; + const newValue = (value || 0) + 1; setValue(newValue); }; diff --git a/packages/react-core/src/components/NumberInput/examples/NumberInputUnit.tsx b/packages/react-core/src/components/NumberInput/examples/NumberInputUnit.tsx index 1fcb8a79c0c..5cab6f02cc5 100644 --- a/packages/react-core/src/components/NumberInput/examples/NumberInputUnit.tsx +++ b/packages/react-core/src/components/NumberInput/examples/NumberInputUnit.tsx @@ -2,26 +2,26 @@ import React from 'react'; import { NumberInput } from '@patternfly/react-core'; export const NumberInputUnit: React.FunctionComponent = () => { - const [value1, setValue1] = React.useState(90); - const [value2, setValue2] = React.useState(Number((1.0).toFixed(2))); + const [value1, setValue1] = React.useState(90); + const [value2, setValue2] = React.useState(Number((1.0).toFixed(2))); - const onMinus1 = () => setValue1(value1 - 1); + const onMinus1 = () => setValue1((value1 || 0) - 1); const onChange1 = (event: React.FormEvent) => { - const target = event.target as HTMLInputElement; - setValue1(Number(target.value)); + const value = (event.target as HTMLInputElement).value; + setValue1(value === '' ? value : +value); }; - const onPlus1 = () => setValue1(value1 + 1); + const onPlus1 = () => setValue1((value1 || 0) + 1); const onMinus2 = () => { - const newValue = Number((value2 - 0.01).toFixed(2)); + const newValue = Number(((value2 || 0) - 0.01).toFixed(2)); setValue2(newValue); }; const onChange2 = (event: React.FormEvent) => { - const target = event.target as HTMLInputElement; - setValue2(Number(target.value)); + const value = (event.target as HTMLInputElement).value; + setValue2(value === '' ? value : +value); }; const onPlus2 = () => { - const newValue = Number((value2 + 0.01).toFixed(2)); + const newValue = Number(((value2 || 0) + 0.01).toFixed(2)); setValue2(newValue); }; diff --git a/packages/react-core/src/components/NumberInput/examples/NumberInputUnitThreshold.tsx b/packages/react-core/src/components/NumberInput/examples/NumberInputUnitThreshold.tsx index 5ef9de026d2..98cf3c66ed9 100644 --- a/packages/react-core/src/components/NumberInput/examples/NumberInputUnitThreshold.tsx +++ b/packages/react-core/src/components/NumberInput/examples/NumberInputUnitThreshold.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { NumberInput } from '@patternfly/react-core'; export const NumberInputUnitThreshold: React.FunctionComponent = () => { - const [value, setValue] = React.useState(0); + const [value, setValue] = React.useState(0); const minValue = 0; const maxValue = 10; @@ -18,18 +18,27 @@ export const NumberInputUnitThreshold: React.FunctionComponent = () => { }; const onMinus = () => { - const newValue = normalizeBetween(value - 1, minValue, maxValue); + const newValue = normalizeBetween((value as number) - 1, minValue, maxValue); setValue(newValue); }; const onChange = (event: React.FormEvent) => { - const target = event.target as HTMLInputElement; - const newValue = normalizeBetween(isNaN(+target.value) ? 0 : Number(target.value), minValue, maxValue); - setValue(newValue); + const value = (event.target as HTMLInputElement).value; + setValue(value === '' ? value : +value); + }; + + const onBlur = (event: React.FocusEvent) => { + const blurVal = +event.target.value; + + if (blurVal < minValue) { + setValue(minValue); + } else if (blurVal > maxValue) { + setValue(maxValue); + } }; const onPlus = () => { - const newValue = normalizeBetween(value + 1, minValue, maxValue); + const newValue = normalizeBetween((value as number) + 1, minValue, maxValue); setValue(newValue); }; @@ -43,6 +52,7 @@ export const NumberInputUnitThreshold: React.FunctionComponent = () => { max={maxValue} onMinus={onMinus} onChange={onChange} + onBlur={onBlur} onPlus={onPlus} inputName="input" inputAriaLabel="number input" diff --git a/packages/react-core/src/components/NumberInput/examples/NumberInputVaryingSizes.tsx b/packages/react-core/src/components/NumberInput/examples/NumberInputVaryingSizes.tsx index ddfa0c9209c..d6430cf9dbe 100644 --- a/packages/react-core/src/components/NumberInput/examples/NumberInputVaryingSizes.tsx +++ b/packages/react-core/src/components/NumberInput/examples/NumberInputVaryingSizes.tsx @@ -2,25 +2,25 @@ import React from 'react'; import { NumberInput } from '@patternfly/react-core'; export const NumberInputVaryingSizes: React.FunctionComponent = () => { - const [input1Value, setInput1Value] = React.useState(1); - const [input2Value, setInput2Value] = React.useState(1234567890); - const [input3Value, setInput3Value] = React.useState(5); - const [input4Value, setInput4Value] = React.useState(12345); + const [input1Value, setInput1Value] = React.useState(1); + const [input2Value, setInput2Value] = React.useState(1234567890); + const [input3Value, setInput3Value] = React.useState(5); + const [input4Value, setInput4Value] = React.useState(12345); const onChange = ( event: React.FormEvent, - updateFunction: React.Dispatch> + updateFunction: React.Dispatch> ) => { - const target = event.target as HTMLInputElement; - updateFunction(Number(target.value)); + const value = (event.target as HTMLInputElement).value; + updateFunction(value === '' ? value : +value); }; - const onMinus = (value, updateFunction: React.Dispatch>) => { - updateFunction(value - 1); + const onMinus = (value: number | '', updateFunction: React.Dispatch>) => { + updateFunction((value || 0) - 1); }; - const onPlus = (value, updateFunction: React.Dispatch>) => { - updateFunction(value + 1); + const onPlus = (value: number | '', updateFunction: React.Dispatch>) => { + updateFunction((value || 0) + 1); }; return ( diff --git a/packages/react-core/src/components/NumberInput/examples/NumberInputWithStatus.tsx b/packages/react-core/src/components/NumberInput/examples/NumberInputWithStatus.tsx index b5b6570a0f8..ef776062074 100644 --- a/packages/react-core/src/components/NumberInput/examples/NumberInputWithStatus.tsx +++ b/packages/react-core/src/components/NumberInput/examples/NumberInputWithStatus.tsx @@ -9,22 +9,21 @@ export const NumberInputWithStatus: React.FunctionComponent = () => { const [value, setValue] = React.useReducer((state, newVal) => Math.max(min, Math.min(max, Number(newVal))), 5); const onPlus = () => { - const newVal = value + 1; + const newVal = (value || 0) + 1; setValue(newVal); validate(newVal); }; const onMinus = () => { - const newVal = value - 1; + const newVal = (value || 0) - 1; setValue(newVal); validate(newVal); }; const onChange = (event: React.FormEvent) => { - const target = event.target as HTMLInputElement; - const newVal = isNaN(+target.value) ? 5 : Number(target.value); - setValue(newVal); - validate(newVal); + const value = (event.target as HTMLInputElement).value; + setValue(value === '' ? value : +value); + validate(value); }; const validate = newVal => { diff --git a/packages/react-integration/cypress/integration/numberInput.spec.ts b/packages/react-integration/cypress/integration/numberInput.spec.ts index 05e6f8c73e2..325973ba35c 100644 --- a/packages/react-integration/cypress/integration/numberInput.spec.ts +++ b/packages/react-integration/cypress/integration/numberInput.spec.ts @@ -60,9 +60,16 @@ describe('NumberInput Demo Test', () => { .should('have.class', 'pf-c-input-group'); }); - it('initial null value can be increment with the plus button', () => { + it('initial undefined value can be increment with the plus button', () => { cy.get('#input3').should('have.value', 0); cy.get('#plus-button3').click(); cy.get('#input3').should('have.value', 1); }); + + it('out of bounds input value can be typed and snap to nearest threshold', () => { + cy.get('#input3').should('have.value', 1); + cy.get('#input3').type('5'); + cy.get('body').click(); + cy.get('#input3').should('have.value', 3); + }); }); diff --git a/packages/react-integration/demo-app-ts/src/components/demos/NumberInputDemo/NumberInputDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/NumberInputDemo/NumberInputDemo.tsx index 68193519109..2811603b4ec 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/NumberInputDemo/NumberInputDemo.tsx +++ b/packages/react-integration/demo-app-ts/src/components/demos/NumberInputDemo/NumberInputDemo.tsx @@ -14,7 +14,7 @@ export class NumberInputDemo extends Component { state: NumberInputDemoState = { value: 0, - value2: null + value2: undefined }; onMinus = () => { @@ -49,10 +49,20 @@ export class NumberInputDemo extends Component { onPlus2 = () => { this.setState({ - value2: this.state.value2 + 1 + value2: (this.state.value2 || 0) + 1 }); }; + onBlur = () => { + if (this.state.value2 > 3) { + this.setState({ + value2: 3 + }); + } else if (this.state.value2 < 0) { + this.setState({ value2: 0 }); + } + }; + render() { const { value, value2 } = this.state; const minValue = 0; @@ -109,6 +119,7 @@ export class NumberInputDemo extends Component { onMinus={this.onMinus2} onChange={this.onChange2} onPlus={this.onPlus2} + onBlur={this.onBlur} inputName="input 3" inputAriaLabel="number input 3" minusBtnAriaLabel="minus"