diff --git a/src/components/form/field_number/__snapshots__/field_number.test.tsx.snap b/src/components/form/field_number/__snapshots__/field_number.test.tsx.snap index a069f5900a2..e097f199fdf 100644 --- a/src/components/form/field_number/__snapshots__/field_number.test.tsx.snap +++ b/src/components/form/field_number/__snapshots__/field_number.test.tsx.snap @@ -17,7 +17,7 @@ exports[`EuiFieldNumber is rendered 1`] = ` max="8" min="1" name="elastic" - step="1" + step="any" type="number" value="1" /> diff --git a/src/components/form/field_number/field_number.spec.tsx b/src/components/form/field_number/field_number.spec.tsx index 66dad13f41e..30ab1b40dd5 100644 --- a/src/components/form/field_number/field_number.spec.tsx +++ b/src/components/form/field_number/field_number.spec.tsx @@ -10,7 +10,8 @@ /// /// -import React from 'react'; +import React, { useState } from 'react'; + import { EuiFieldNumber } from './field_number'; describe('EuiFieldNumber', () => { @@ -52,19 +53,124 @@ describe('EuiFieldNumber', () => { checkIsInvalid(); }); - it('shows invalid state on blur', () => { - cy.mount(); - checkIsValid(); - cy.get('input').click(); - cy.get('body').click('bottomRight'); - checkIsInvalid(); - }); - it('does not show invalid state on decimal values by default', () => { cy.mount(); checkIsValid(); cy.get('input').click().type('1.5'); checkIsValid(); }); + + it('checks/updates invalid state for controlled components', () => { + const ControlledEuiFieldNumber = () => { + const [value, setValue] = useState('0'); + return ( + <> + setValue(e.target.value)} + min={0} + max={5} + /> + + + + ); + }; + cy.mount(); + checkIsValid(); + + // Controlled value changes should work as expected + cy.get('#setToInvalidValue').click(); + checkIsInvalid(); + cy.get('#setToValidValue').click(); + checkIsValid(); + + // (regression test) User input changes should still work as expected w/ onChange + cy.get('input').clear().type('-2'); + checkIsInvalid(); + cy.get('input').clear().type('2'); + checkIsValid(); + }); + + describe('checks/updates invalid state when props that would affect validity change', () => { + it('min', () => { + const UpdatedEuiFieldNumber = () => { + const [min, setMin] = useState(); + return ( + <> + + + + + ); + }; + cy.mount(); + cy.get('input').type('1'); + checkIsValid(); + + cy.get('#setInvalidMin').click(); + checkIsInvalid(); + cy.get('#setValidMin').click(); + checkIsValid(); + }); + + it('max', () => { + const UpdatedEuiFieldNumber = () => { + const [max, setMax] = useState(); + return ( + <> + + + + + ); + }; + cy.mount(); + cy.get('input').type('1'); + checkIsValid(); + + cy.get('#setInvalidMax').click(); + checkIsInvalid(); + cy.get('#setValidMax').click(); + checkIsValid(); + }); + + it('step', () => { + const UpdatedEuiFieldNumber = () => { + const [step, setStep] = useState(); + return ( + <> + + + + + ); + }; + cy.mount(); + cy.get('input').type('1'); + checkIsValid(); + + cy.get('#setInvalidStep').click(); + checkIsInvalid(); + cy.get('#setValidStep').click(); + checkIsValid(); + }); + }); }); }); diff --git a/src/components/form/field_number/field_number.stories.tsx b/src/components/form/field_number/field_number.stories.tsx new file mode 100644 index 00000000000..bd1f86380d9 --- /dev/null +++ b/src/components/form/field_number/field_number.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiFieldNumber, EuiFieldNumberProps } from './field_number'; + +const meta: Meta = { + title: 'EuiFieldNumber', + component: EuiFieldNumber, + argTypes: { + step: { control: 'number' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + +export const ControlledComponent: Story = { + args: { + value: 0, + }, + argTypes: { + value: { control: 'number' }, + onChange: () => {}, + }, +}; diff --git a/src/components/form/field_number/field_number.test.tsx b/src/components/form/field_number/field_number.test.tsx index 5ce42c0715e..e5870d92c0e 100644 --- a/src/components/form/field_number/field_number.test.tsx +++ b/src/components/form/field_number/field_number.test.tsx @@ -32,7 +32,10 @@ describe('EuiFieldNumber', () => { name="elastic" min={1} max={8} - step={1} + // TODO: Restore this once we upgrade Jest/jsdom to v15+. Right now passing + // a `step` prop always leads to jsdom thinking that validity.valid is false + // @see https://github.com/jsdom/jsdom/issues/2288 + // step={1} value={1} icon="warning" onChange={() => {}} diff --git a/src/components/form/field_number/field_number.tsx b/src/components/form/field_number/field_number.tsx index b4e6341eba1..f7a6f9e003f 100644 --- a/src/components/form/field_number/field_number.tsx +++ b/src/components/form/field_number/field_number.tsx @@ -11,11 +11,14 @@ import React, { Ref, FunctionComponent, useState, + useEffect, useCallback, + useRef, } from 'react'; -import { CommonProps } from '../../common'; import classNames from 'classnames'; +import { useCombinedRefs } from '../../../services'; +import { CommonProps } from '../../common'; import { IconType } from '../../icon'; import { EuiValidatableControl } from '../validatable_control'; @@ -104,10 +107,12 @@ export const EuiFieldNumber: FunctionComponent = ( readOnly, controlOnly, onKeyUp, - onBlur, ...rest } = props; + const _inputRef = useRef(null); + const combinedRefs = useCombinedRefs([_inputRef, inputRef]); + // Attempt to determine additional invalid state. The native number input // will set :invalid state automatically, but we need to also set // `aria-invalid` as well as display an icon. We also want to *not* set this on @@ -122,6 +127,13 @@ export const EuiFieldNumber: FunctionComponent = ( setIsNativelyInvalid(isInvalid); }, []); + // Re-check validity whenever props that might affect validity are updated + useEffect(() => { + if (_inputRef.current) { + checkNativeValidity(_inputRef.current); + } + }, [value, min, max, step, checkNativeValidity]); + const numIconsClass = controlOnly ? false : getFormControlClassNameForIconCount({ @@ -150,7 +162,7 @@ export const EuiFieldNumber: FunctionComponent = ( placeholder={placeholder} readOnly={readOnly} className={classes} - ref={inputRef} + ref={combinedRefs} aria-invalid={isInvalid || isNativelyInvalid} onKeyUp={(e) => { // Note that we can't use `onChange` because browsers don't emit change events @@ -158,11 +170,6 @@ export const EuiFieldNumber: FunctionComponent = ( onKeyUp?.(e); checkNativeValidity(e.currentTarget); }} - onBlur={(e) => { - // Browsers can also set/determine validity (e.g. when `step` is undefined) on focus blur - onBlur?.(e); - checkNativeValidity(e.currentTarget); - }} {...rest} /> diff --git a/src/components/form/range/__snapshots__/dual_range.test.tsx.snap b/src/components/form/range/__snapshots__/dual_range.test.tsx.snap index 388ee782b74..d7167838120 100644 --- a/src/components/form/range/__snapshots__/dual_range.test.tsx.snap +++ b/src/components/form/range/__snapshots__/dual_range.test.tsx.snap @@ -224,7 +224,8 @@ exports[`EuiDualRange props maxInputProps allows overriding default props 1`] = class="euiFormControlLayout__childrenWrapper" > +
+ +
@@ -318,8 +327,9 @@ exports[`EuiDualRange props maxInputProps applies passed props to max input 1`] class="euiFormControlLayout__childrenWrapper" > +
+ +
@@ -344,7 +362,8 @@ exports[`EuiDualRange props minInputProps allows overriding default props 1`] = class="euiFormControlLayout__childrenWrapper" > +
+ +
+
+ +
@@ -508,7 +544,8 @@ exports[`EuiDualRange props minInputProps applies passed props to min input 1`] class="euiFormControlLayout__childrenWrapper" > +
+ +
diff --git a/upcoming_changelogs/7291.md b/upcoming_changelogs/7291.md new file mode 100644 index 00000000000..cdd132adae5 --- /dev/null +++ b/upcoming_changelogs/7291.md @@ -0,0 +1,3 @@ +**Bug fixes** + +- Fixed controlled `EuiFieldNumbers` not correctly updating native validity state