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